<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Python GUIs - chat</title><link href="https://www.pythonguis.com/" rel="alternate"/><link href="https://www.pythonguis.com/feeds/chat.tag.atom.xml" rel="self"/><id>https://www.pythonguis.com/</id><updated>2020-07-05T09:00:00+00:00</updated><subtitle>Create GUI applications with Python and Qt</subtitle><entry><title>Chat Speech Bubbles in PyQt6 with QListView and Custom Delegates — Build a messaging-style interface with colored speech bubbles, timestamps, and left/right alignment</title><link href="https://www.pythonguis.com/faq/cloud-around-the-text-in-qtextedit/" rel="alternate"/><published>2020-07-05T09:00:00+00:00</published><updated>2020-07-05T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2020-07-05:/faq/cloud-around-the-text-in-qtextedit/</id><summary type="html">How can I add speech bubbles like Telegram or WhatsApp around messages in a Python Qt application? I'm currently using a QTextEdit, but I'd like each message to appear in its own styled bubble.</summary><content type="html">
            &lt;blockquote&gt;
&lt;p&gt;How can I add speech bubbles like Telegram or WhatsApp around messages in a Python Qt application? I'm currently using a QTextEdit, but I'd like each message to appear in its own styled bubble.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A &lt;code&gt;QTextEdit&lt;/code&gt; treats everything as one continuous block of text, which makes it awkward to style individual messages with their own bubbles, colors, and positions. A much better fit for a chat interface is a &lt;code&gt;QListView&lt;/code&gt;, where each message is a separate item in a list. You can then use a &lt;strong&gt;custom delegate&lt;/strong&gt; to paint each message with its own speech bubble.&lt;/p&gt;
&lt;p&gt;In this tutorial, you'll build a chat-style message view from scratch using PyQt6. By the end, you'll have colored speech bubbles that wrap text properly, align to the left or right depending on the sender, and display timestamps &amp;mdash; just like a real messaging app.&lt;/p&gt;
&lt;h2 id="setting-up-the-model"&gt;Setting up the model&lt;/h2&gt;
&lt;p&gt;Qt's Model/View architecture separates data from presentation. We'll store our messages in a simple list model, and let a delegate handle all the drawing.&lt;/p&gt;
&lt;p&gt;Each message is a tuple of &lt;code&gt;(user, text)&lt;/code&gt;, where &lt;code&gt;user&lt;/code&gt; is either &lt;code&gt;USER_ME&lt;/code&gt; (0) or &lt;code&gt;USER_THEM&lt;/code&gt; (1). This tells the delegate which side of the window to draw the bubble on and what color to use.&lt;/p&gt;
&lt;p&gt;Here's the model:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from PyQt6.QtCore import QAbstractListModel, Qt


USER_ME = 0
USER_THEM = 1


class MessageModel(QAbstractListModel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.messages = []

    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole:
            return self.messages[index.row()]

    def rowCount(self, index):
        return len(self.messages)

    def add_message(self, who, text):
        """
        Add a message to the list.
        """
        if text:
            self.messages.append((who, text))
            self.layoutChanged.emit()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;data&lt;/code&gt; method returns the full &lt;code&gt;(user, text)&lt;/code&gt; tuple for each row. The delegate will unpack this when it paints.&lt;/p&gt;
&lt;p&gt;If you're not familiar with Qt's Model/View system, take a look at the &lt;a href="https://www.pythonguis.com/tutorials/modelview-architecture/"&gt;Model View Architecture tutorial&lt;/a&gt; for background.&lt;/p&gt;
&lt;h2 id="drawing-bubbles-with-a-custom-delegate"&gt;Drawing bubbles with a custom delegate&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;QStyledItemDelegate&lt;/code&gt; lets you take full control over how each item in a &lt;code&gt;QListView&lt;/code&gt; is drawn. We override two methods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;paint()&lt;/code&gt; &amp;mdash; draws the bubble and text.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sizeHint()&lt;/code&gt; &amp;mdash; tells the view how tall each item should be.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;A first version&lt;/h3&gt;
&lt;p&gt;Let's start with a simple delegate that draws a rounded rectangle (the bubble) and the message text inside it:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from PyQt6.QtCore import QMargins, QPoint, QRectF
from PyQt6.QtGui import QColor, QTextDocument, QTextOption
from PyQt6.QtWidgets import QStyledItemDelegate


BUBBLE_COLORS = {USER_ME: "#90caf9", USER_THEM: "#a5d6a7"}

BUBBLE_PADDING = QMargins(15, 5, 15, 5)
TEXT_PADDING = QMargins(25, 15, 25, 15)


class MessageDelegate(QStyledItemDelegate):

    def paint(self, painter, option, index):
        painter.save()

        user, text = index.model().data(index, Qt.ItemDataRole.DisplayRole)

        bubblerect = option.rect.marginsRemoved(BUBBLE_PADDING)
        textrect = option.rect.marginsRemoved(TEXT_PADDING)

        # Draw the bubble background.
        painter.setPen(Qt.PenStyle.NoPen)
        color = QColor(BUBBLE_COLORS[user])
        painter.setBrush(color)
        painter.drawRoundedRect(bubblerect, 10, 10)

        # Draw a small triangle pointer on the bubble.
        if user == USER_ME:
            p1 = bubblerect.topRight()
        else:
            p1 = bubblerect.topLeft()

        painter.drawPolygon(
            [p1 + QPoint(-20, 0), p1 + QPoint(20, 0), p1 + QPoint(0, 20)]
        )

        # Draw the message text.
        toption = QTextOption()
        toption.setWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)

        doc = QTextDocument(text)
        doc.setTextWidth(textrect.width())
        doc.setDefaultTextOption(toption)
        doc.setDocumentMargin(0)

        painter.translate(textrect.topLeft())
        doc.drawContents(painter)
        painter.restore()

    def sizeHint(self, option, index):
        _, text = index.model().data(index, Qt.ItemDataRole.DisplayRole)
        textrect = option.rect.marginsRemoved(TEXT_PADDING)

        toption = QTextOption()
        toption.setWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)

        doc = QTextDocument(text)
        doc.setTextWidth(textrect.width())
        doc.setDefaultTextOption(toption)
        doc.setDocumentMargin(0)

        textrect.setHeight(int(doc.size().height()))
        textrect = textrect.marginsAdded(TEXT_PADDING)
        return textrect.size()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;There are a few things worth noting here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;QTextDocument&lt;/code&gt; for text layout.&lt;/strong&gt; We use &lt;code&gt;QTextDocument&lt;/code&gt; both in &lt;code&gt;paint()&lt;/code&gt; and &lt;code&gt;sizeHint()&lt;/code&gt;. This ensures the text wrapping calculation matches the actual rendering exactly. Earlier approaches using &lt;code&gt;QFontMetrics&lt;/code&gt; and &lt;code&gt;painter.drawText()&lt;/code&gt; separately led to mismatches where text would get clipped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;WrapAtWordBoundaryOrAnywhere&lt;/code&gt;.&lt;/strong&gt; This wrap mode breaks text between words when possible, but will also break mid-word if a single word is longer than the available width. This prevents long strings from overflowing the bubble.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;painter.save()&lt;/code&gt; and &lt;code&gt;painter.restore()&lt;/code&gt;.&lt;/strong&gt; Because we call &lt;code&gt;painter.translate()&lt;/code&gt; to position the text, we need to save and restore the painter state. Otherwise, each successive item would be drawn at an offset from the previous one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Padding with &lt;code&gt;QMargins&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;BUBBLE_PADDING&lt;/code&gt; controls the space between the edge of the list item and the bubble. &lt;code&gt;TEXT_PADDING&lt;/code&gt; controls the space between the edge of the list item and the text. The difference between the two creates an inner margin inside the bubble.&lt;/p&gt;
&lt;h2 id="aligning-bubbles-left-and-right"&gt;Aligning bubbles left and right&lt;/h2&gt;
&lt;p&gt;In a real chat app, your own messages appear on the right and other people's messages appear on the left. We can achieve this by translating the painter's origin before drawing, and adjusting the padding to leave room on the appropriate side.&lt;/p&gt;
&lt;p&gt;Add a translation mapping at the top of the file:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;USER_TRANSLATE = {USER_ME: QPoint(20, 0), USER_THEM: QPoint(0, 0)}

BUBBLE_PADDING = QMargins(15, 5, 35, 5)
TEXT_PADDING = QMargins(25, 15, 45, 15)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The extra padding on the right side (35 and 45 instead of 15 and 25) creates space for the offset. &lt;code&gt;USER_ME&lt;/code&gt; messages are shifted 20 pixels to the right; &lt;code&gt;USER_THEM&lt;/code&gt; messages stay at the left edge.&lt;/p&gt;
&lt;p&gt;In the &lt;code&gt;paint()&lt;/code&gt; method, apply the translation right after saving the painter:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;    def paint(self, painter, option, index):
        painter.save()

        user, text = index.model().data(index, Qt.ItemDataRole.DisplayRole)

        # Shift the bubble left or right depending on the sender.
        trans = USER_TRANSLATE[user]
        painter.translate(trans)

        # ... rest of the painting code stays the same.
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Now "me" messages appear shifted to the right, and "them" messages sit on the left &amp;mdash; just like a real messaging app.&lt;/p&gt;
&lt;h2 id="adding-timestamps"&gt;Adding timestamps&lt;/h2&gt;
&lt;p&gt;To display a timestamp under each message, you need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Store the timestamp alongside the message in the model.&lt;/li&gt;
&lt;li&gt;Draw it in the delegate's &lt;code&gt;paint()&lt;/code&gt; method.&lt;/li&gt;
&lt;li&gt;Add extra height in &lt;code&gt;sizeHint()&lt;/code&gt; to make room for it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Update the model to accept a third value &amp;mdash; the timestamp:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from time import time


class MessageModel(QAbstractListModel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.messages = []

    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole:
            return self.messages[index.row()]

    def rowCount(self, index):
        return len(self.messages)

    def add_message(self, who, text):
        if text:
            self.messages.append((who, text, time()))
            self.layoutChanged.emit()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;time()&lt;/code&gt; returns the current Unix timestamp as a float. We store it alongside the user and text.&lt;/p&gt;
&lt;p&gt;Update &lt;code&gt;sizeHint()&lt;/code&gt; to add space for the timestamp line. We just need an extra 20 pixels of height:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;    def sizeHint(self, option, index):
        _, text, _ = index.model().data(index, Qt.ItemDataRole.DisplayRole)
        textrect = option.rect.marginsRemoved(TEXT_PADDING)

        toption = QTextOption()
        toption.setWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)

        doc = QTextDocument(text)
        doc.setTextWidth(textrect.width())
        doc.setDefaultTextOption(toption)
        doc.setDocumentMargin(0)

        textrect.setHeight(int(doc.size().height()))
        textrect = textrect.marginsAdded(TEXT_PADDING)
        return textrect.size() + QSize(0, 20)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;And update &lt;code&gt;paint()&lt;/code&gt; to draw the timestamp text below the message. We position it at the bottom-left of the text rectangle, with a small vertical offset:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from datetime import datetime

# Inside paint(), after drawing the bubble but before drawing the message text:

        # Draw the timestamp in a smaller font.
        font = painter.font()
        font.setPointSize(7)
        painter.setFont(font)
        painter.setPen(Qt.GlobalColor.black)
        time_str = datetime.fromtimestamp(timestamp).strftime("%H:%M")
        painter.drawText(textrect.bottomLeft() + QPoint(0, 5), time_str)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The timestamp appears just below the message, inside the bubble.&lt;/p&gt;
&lt;h2 id="adding-spacing-between-bubbles"&gt;Adding spacing between bubbles&lt;/h2&gt;
&lt;p&gt;By default, items in a &lt;code&gt;QListView&lt;/code&gt; sit right next to each other. To add vertical spacing between bubbles, use the &lt;code&gt;setSpacing()&lt;/code&gt; method on the list view:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;self.messages = QListView()
self.messages.setSpacing(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;This adds 5 pixels of space around each item. Adjust the value to suit your design.&lt;/p&gt;
&lt;h2 id="complete-working-example"&gt;Complete working example&lt;/h2&gt;
&lt;p&gt;Here's the full application with colored bubbles, left/right alignment, timestamps, and spacing:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
from datetime import datetime
from time import time

from PyQt6.QtCore import (
    QAbstractListModel,
    QMargins,
    QPoint,
    QSize,
    Qt,
)
from PyQt6.QtGui import QColor, QTextDocument, QTextOption
from PyQt6.QtWidgets import (
    QApplication,
    QLineEdit,
    QListView,
    QMainWindow,
    QPushButton,
    QStyledItemDelegate,
    QVBoxLayout,
    QWidget,
)

USER_ME = 0
USER_THEM = 1

BUBBLE_COLORS = {USER_ME: "#90caf9", USER_THEM: "#a5d6a7"}
USER_TRANSLATE = {USER_ME: QPoint(20, 0), USER_THEM: QPoint(0, 0)}

BUBBLE_PADDING = QMargins(15, 5, 35, 5)
TEXT_PADDING = QMargins(25, 15, 45, 15)


class MessageDelegate(QStyledItemDelegate):
    """
    Draws each message as a speech bubble.
    """

    def paint(self, painter, option, index):
        painter.save()

        user, text, timestamp = index.model().data(
            index, Qt.ItemDataRole.DisplayRole
        )

        # Shift the bubble depending on the sender.
        trans = USER_TRANSLATE[user]
        painter.translate(trans)

        bubblerect = option.rect.marginsRemoved(BUBBLE_PADDING)
        textrect = option.rect.marginsRemoved(TEXT_PADDING)

        # Draw the bubble background.
        painter.setPen(Qt.PenStyle.NoPen)
        color = QColor(BUBBLE_COLORS[user])
        painter.setBrush(color)
        painter.drawRoundedRect(bubblerect, 10, 10)

        # Draw the triangle pointer.
        if user == USER_ME:
            p1 = bubblerect.topRight()
        else:
            p1 = bubblerect.topLeft()

        painter.drawPolygon(
            [p1 + QPoint(-20, 0), p1 + QPoint(20, 0), p1 + QPoint(0, 20)]
        )

        # Draw the timestamp below the text area.
        font = painter.font()
        font.setPointSize(7)
        painter.setFont(font)
        painter.setPen(Qt.GlobalColor.black)
        time_str = datetime.fromtimestamp(timestamp).strftime("%H:%M")
        painter.drawText(textrect.bottomLeft() + QPoint(0, 5), time_str)

        # Draw the message text using QTextDocument for proper wrapping.
        toption = QTextOption()
        toption.setWrapMode(
            QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere
        )

        doc = QTextDocument(text)
        doc.setTextWidth(textrect.width())
        doc.setDefaultTextOption(toption)
        doc.setDocumentMargin(0)

        painter.translate(textrect.topLeft())
        doc.drawContents(painter)
        painter.restore()

    def sizeHint(self, option, index):
        _, text, _ = index.model().data(
            index, Qt.ItemDataRole.DisplayRole
        )
        textrect = option.rect.marginsRemoved(TEXT_PADDING)

        toption = QTextOption()
        toption.setWrapMode(
            QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere
        )

        doc = QTextDocument(text)
        doc.setTextWidth(textrect.width())
        doc.setDefaultTextOption(toption)
        doc.setDocumentMargin(0)

        textrect.setHeight(int(doc.size().height()))
        textrect = textrect.marginsAdded(TEXT_PADDING)
        # Add 20px for the timestamp line.
        return textrect.size() + QSize(0, 20)


class MessageModel(QAbstractListModel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.messages = []

    def data(self, index, role):
        if role == Qt.ItemDataRole.DisplayRole:
            return self.messages[index.row()]

    def rowCount(self, index):
        return len(self.messages)

    def add_message(self, who, text):
        """
        Add a message to the chat.
        """
        if text:
            self.messages.append((who, text, time()))
            self.layoutChanged.emit()


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Chat Bubbles")

        layout = QVBoxLayout()

        self.message_input = QLineEdit()
        self.message_input.setPlaceholderText("Type a message...")

        # Buttons to simulate sending and receiving messages.
        self.btn_send = QPushButton("Send (Me) &amp;rarr;")
        self.btn_receive = QPushButton("&amp;larr; Receive (Them)")

        self.message_list = QListView()
        self.message_list.setSpacing(5)
        self.message_list.setResizeMode(QListView.ResizeMode.Adjust)
        self.message_list.setItemDelegate(MessageDelegate())

        self.model = MessageModel()
        self.message_list.setModel(self.model)

        self.btn_send.pressed.connect(self.send_message)
        self.btn_receive.pressed.connect(self.receive_message)

        layout.addWidget(self.message_list)
        layout.addWidget(self.message_input)
        layout.addWidget(self.btn_send)
        layout.addWidget(self.btn_receive)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

    def resizeEvent(self, e):
        # Recalculate bubble sizes when the window is resized.
        self.model.layoutChanged.emit()

    def send_message(self):
        self.model.add_message(USER_ME, self.message_input.text())
        self.message_input.clear()

    def receive_message(self):
        self.model.add_message(USER_THEM, self.message_input.text())
        self.message_input.clear()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Run this and type a message into the text field. Press &lt;strong&gt;Send (Me) &amp;rarr;&lt;/strong&gt; to add a blue bubble on the right, or &lt;strong&gt;&amp;larr; Receive (Them)&lt;/strong&gt; to add a green bubble on the left. Each bubble displays the message text with proper word wrapping and a timestamp underneath.&lt;/p&gt;
&lt;h2 id="how-it-all-fits-together"&gt;How it all fits together&lt;/h2&gt;
&lt;p&gt;The architecture follows Qt's Model/View pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MessageModel&lt;/code&gt;&lt;/strong&gt; holds a list of &lt;code&gt;(user, text, timestamp)&lt;/code&gt; tuples. It provides data to the view when requested.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;QListView&lt;/code&gt;&lt;/strong&gt; displays the items and handles scrolling and layout.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MessageDelegate&lt;/code&gt;&lt;/strong&gt; controls the visual appearance of each item. It uses &lt;code&gt;QTextDocument&lt;/code&gt; for accurate text measurement and rendering, and draws the bubble shape with &lt;code&gt;QPainter&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you call &lt;code&gt;add_message()&lt;/code&gt;, the model appends the new message and emits &lt;code&gt;layoutChanged&lt;/code&gt;, which tells the view to refresh. The view asks the delegate for each item's size (via &lt;code&gt;sizeHint&lt;/code&gt;) and then asks it to paint each visible item.&lt;/p&gt;
&lt;p&gt;This approach scales well. You can extend it to support features like different fonts, inline images, read receipts, or message status indicators &amp;mdash; all by adding data to the model and updating the delegate's paint logic.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt6 see my book, &lt;a href="https://www.mfitzp.com/pyqt6-book/"&gt;Create GUI Applications with Python &amp; Qt6.&lt;/a&gt;&lt;/p&gt;
            </content><category term="pyqt6"/><category term="python"/><category term="qlistview"/><category term="delegate"/><category term="qtextdocument"/><category term="chat"/><category term="gui"/><category term="qt"/><category term="qt6"/></entry></feed>