<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Python GUIs - Context-Menu</title><link href="https://www.pythonguis.com/" rel="alternate"/><link href="https://www.pythonguis.com/feeds/context-menu.tag.atom.xml" rel="self"/><id>https://www.pythonguis.com/</id><updated>2021-03-25T09:00:00+00:00</updated><subtitle>Create GUI applications with Python and Qt</subtitle><entry><title>Implementing "Open With" Context Menus in PyQt6 — Query your Linux desktop for available applications and let users choose how to open files</title><link href="https://www.pythonguis.com/faq/right-click-open-with/" rel="alternate"/><published>2021-03-25T09:00:00+00:00</published><updated>2021-03-25T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2021-03-25:/faq/right-click-open-with/</id><summary type="html">I'm finishing an application that has a custom context menu with "Open" and "Open Containing Folder" working. I can't seem to get an "Open With" option like you see in file explorers. Depending on the file type there would be different options &amp;mdash; so I don't think we're talking about predefined lists, but rather something extracted from the host OS. Any help with "Open With" would be greatly appreciated.</summary><content type="html">
            &lt;blockquote&gt;
&lt;p&gt;I'm finishing an application that has a custom context menu with "Open" and "Open Containing Folder" working. I can't seem to get an "Open With" option like you see in file explorers. Depending on the file type there would be different options &amp;mdash; so I don't think we're talking about predefined lists, but rather something extracted from the host OS. Any help with "Open With" would be greatly appreciated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When you right-click a file in a file manager like Nemo or Nautilus, you see a list of applications that can handle that file type. That list comes from the operating system's knowledge of installed applications and their supported MIME types. To recreate this in your own PyQt6 application, you need to query that same information.&lt;/p&gt;
&lt;p&gt;On Linux desktops (GNOME, Cinnamon, XFCE, etc.), this information is managed through the &lt;strong&gt;freedesktop.org&lt;/strong&gt; standards. The &lt;code&gt;gio&lt;/code&gt; module from &lt;strong&gt;PyGObject&lt;/strong&gt; (the Python bindings for GLib/GIO) gives you clean access to all of it &amp;mdash; which applications support a given MIME type, what their names and icons are, and how to launch them.&lt;/p&gt;
&lt;p&gt;Let's walk through building this step by step.&lt;/p&gt;
&lt;h2 id="understanding-the-moving-parts"&gt;Understanding the moving parts&lt;/h2&gt;
&lt;p&gt;There are three things you need to do:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Determine the MIME type&lt;/strong&gt; of a file (e.g., &lt;code&gt;image/png&lt;/code&gt;, &lt;code&gt;text/plain&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Find the applications&lt;/strong&gt; that can open that MIME type.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Launch the chosen application&lt;/strong&gt; with the file as an argument.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The GIO library handles all three. If you're on a Linux desktop with GTK-based tools installed, you almost certainly have &lt;code&gt;gi&lt;/code&gt; (PyGObject) available already. If not, you can install it:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-sh"&gt;sh&lt;/span&gt;
&lt;pre&gt;&lt;code class="sh"&gt;sudo apt install python3-gi gir1.2-gio-2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id="getting-the-mime-type-for-a-file"&gt;Getting the MIME type for a file&lt;/h2&gt;
&lt;p&gt;GIO can detect the MIME type of a file by inspecting its contents and name:&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 gi.repository import Gio

def get_mime_type(filepath):
    """Return the MIME type string for a given file path."""
    gfile = Gio.File.new_for_path(filepath)
    info = gfile.query_info(
        "standard::content-type", Gio.FileQueryInfoFlags.NONE, None
    )
    return info.get_content_type()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Calling &lt;code&gt;get_mime_type("/home/user/photo.png")&lt;/code&gt; would return something like &lt;code&gt;"image/png"&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="finding-applications-for-a-mime-type"&gt;Finding applications for a MIME type&lt;/h2&gt;
&lt;p&gt;Once you have the MIME type, you can ask GIO for a list of applications that support 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 gi.repository import Gio

def get_apps_for_mime(mime_type):
    """Return a list of Gio.AppInfo objects that can open the given MIME type."""
    return Gio.AppInfo.get_all_for_type(mime_type)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Each &lt;code&gt;Gio.AppInfo&lt;/code&gt; object gives you the application's display name (e.g., "GIMP Image Editor"), its icon, and the ability to launch it with a file.&lt;/p&gt;
&lt;h2 id="launching-an-application-with-a-file"&gt;Launching an application with a file&lt;/h2&gt;
&lt;p&gt;To open a file with a specific application:&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 gi.repository import Gio

def open_file_with_app(app_info, filepath):
    """Launch the given app with the specified file."""
    gfile = Gio.File.new_for_path(filepath)
    app_info.launch([gfile], None)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;That's the complete backend. Now let's wire it into a PyQt6 context menu.&lt;/p&gt;
&lt;h2 id="building-the-context-menu-in-pyqt6"&gt;Building the context menu in PyQt6&lt;/h2&gt;
&lt;p&gt;The idea is straightforward: when the user right-clicks, you build a &lt;code&gt;QMenu&lt;/code&gt; that includes an "Open With" submenu. That submenu is populated dynamically based on the file they clicked on. If you're new to building menus and actions in PyQt6, the &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-actions-toolbars-menus/"&gt;Actions, Toolbars &amp;amp; Menus tutorial&lt;/a&gt; covers the fundamentals.&lt;/p&gt;
&lt;p&gt;Here's a minimal example using a &lt;code&gt;QListWidget&lt;/code&gt; to display some file paths. Right-clicking a file shows the context menu with an "Open With" submenu:&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
import os
import subprocess

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QListWidget,
    QMainWindow,
    QMenu,
)

from gi.repository import Gio


def get_mime_type(filepath):
    """Return the MIME type string for a given file path."""
    gfile = Gio.File.new_for_path(filepath)
    info = gfile.query_info(
        "standard::content-type", Gio.FileQueryInfoFlags.NONE, None
    )
    return info.get_content_type()


def get_apps_for_mime(mime_type):
    """Return a list of Gio.AppInfo objects for the given MIME type."""
    return Gio.AppInfo.get_all_for_type(mime_type)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Open With Example")
        self.resize(500, 400)

        self.list_widget = QListWidget()
        self.setCentralWidget(self.list_widget)

        # Add some example files &amp;mdash; replace these with real paths on your system.
        example_files = [
            os.path.expanduser("~/Documents"),
            os.path.expanduser("~/Pictures"),
        ]

        # Walk through directories and add files.
        for folder in example_files:
            if os.path.isdir(folder):
                for entry in os.listdir(folder):
                    full_path = os.path.join(folder, entry)
                    if os.path.isfile(full_path):
                        self.list_widget.addItem(full_path)

        self.list_widget.setContextMenuPolicy(
            Qt.ContextMenuPolicy.CustomContextMenu
        )
        self.list_widget.customContextMenuRequested.connect(
            self.show_context_menu
        )

    def show_context_menu(self, position):
        item = self.list_widget.itemAt(position)
        if item is None:
            return

        filepath = item.text()

        menu = QMenu(self)

        # "Open" action &amp;mdash; uses the system default application.
        open_action = menu.addAction("Open")
        open_action.triggered.connect(
            lambda: self.open_default(filepath)
        )

        # "Open Containing Folder" action.
        open_folder_action = menu.addAction("Open Containing Folder")
        open_folder_action.triggered.connect(
            lambda: self.open_containing_folder(filepath)
        )

        # "Open With" submenu.
        open_with_menu = menu.addMenu("Open With...")
        self.populate_open_with_menu(open_with_menu, filepath)

        menu.exec(self.list_widget.mapToGlobal(position))

    def populate_open_with_menu(self, submenu, filepath):
        """Fill the 'Open With' submenu with available applications."""
        try:
            mime_type = get_mime_type(filepath)
        except Exception:
            no_apps = submenu.addAction("(unable to detect file type)")
            no_apps.setEnabled(False)
            return

        apps = get_apps_for_mime(mime_type)

        if not apps:
            no_apps = submenu.addAction("(no applications found)")
            no_apps.setEnabled(False)
            return

        for app_info in apps:
            app_name = app_info.get_display_name()
            action = submenu.addAction(app_name)
            # Use a default argument in the lambda to capture the current
            # app_info, avoiding the common closure-in-a-loop issue.
            action.triggered.connect(
                lambda checked, ai=app_info: self.open_with_app(
                    ai, filepath
                )
            )

    def open_default(self, filepath):
        """Open the file with the system default application."""
        subprocess.Popen(["xdg-open", filepath])

    def open_containing_folder(self, filepath):
        """Open the folder containing the file."""
        folder = os.path.dirname(filepath)
        subprocess.Popen(["xdg-open", folder])

    def open_with_app(self, app_info, filepath):
        """Launch a specific application with the given file."""
        gfile = Gio.File.new_for_path(filepath)
        try:
            app_info.launch([gfile], None)
        except Exception as e:
            print(f"Failed to launch {app_info.get_display_name()}: {e}")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;You can copy this, update the &lt;code&gt;example_files&lt;/code&gt; list with folders that exist on your system, and run it. Right-clicking any file in the list will show a context menu with the full "Open With..." submenu populated from your installed applications.&lt;/p&gt;
&lt;h2 id="how-the-lambda-capture-works"&gt;How the lambda capture works&lt;/h2&gt;
&lt;p&gt;You might have noticed this pattern in the loop:&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;action.triggered.connect(
    lambda checked, ai=app_info: self.open_with_app(ai, filepath)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;ai=app_info&lt;/code&gt; part is a default argument that captures the &lt;em&gt;current&lt;/em&gt; value of &lt;code&gt;app_info&lt;/code&gt; at the time the lambda is created. Without it, every menu action would end up using the &lt;em&gt;last&lt;/em&gt; value of &lt;code&gt;app_info&lt;/code&gt; from the loop &amp;mdash; a common and frustrating Python gotcha when connecting signals inside loops. For more on how signals and slots work in PyQt6, including lambda connections, see &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-signals-slots-events/"&gt;Signals, Slots &amp;amp; Events&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="adding-application-icons"&gt;Adding application icons&lt;/h2&gt;
&lt;p&gt;You can make the submenu look more polished by including application icons. GIO provides icon information through &lt;code&gt;app_info.get_icon()&lt;/code&gt;, and you can resolve this to a file path using the system icon theme. Here's an updated version of the &lt;code&gt;populate_open_with_menu&lt;/code&gt; method:&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.QtGui import QIcon


def populate_open_with_menu(self, submenu, filepath):
    """Fill the 'Open With' submenu with available applications."""
    try:
        mime_type = get_mime_type(filepath)
    except Exception:
        action = submenu.addAction("(unable to detect file type)")
        action.setEnabled(False)
        return

    apps = get_apps_for_mime(mime_type)

    if not apps:
        action = submenu.addAction("(no applications found)")
        action.setEnabled(False)
        return

    for app_info in apps:
        app_name = app_info.get_display_name()
        icon = app_info.get_icon()

        action = submenu.addAction(app_name)

        # Try to load the icon from the system icon theme.
        if icon is not None and hasattr(icon, "get_names"):
            for icon_name in icon.get_names():
                qt_icon = QIcon.fromTheme(icon_name)
                if not qt_icon.isNull():
                    action.setIcon(qt_icon)
                    break

        action.triggered.connect(
            lambda checked, ai=app_info: self.open_with_app(
                ai, filepath
            )
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;QIcon.fromTheme()&lt;/code&gt; looks up icons from the system's icon theme (the same one your desktop environment uses), so the icons in your submenu will match what users see in their native file manager.&lt;/p&gt;
&lt;h2 id="a-note-on-cross-platform-compatibility"&gt;A note on cross-platform compatibility&lt;/h2&gt;
&lt;p&gt;This approach relies on GIO, which is a GLib/GNOME technology. It works well on Linux desktops that follow freedesktop.org standards, which includes Linux Mint, Ubuntu, Fedora, and most others.&lt;/p&gt;
&lt;p&gt;If you need cross-platform support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On macOS&lt;/strong&gt;, you can use &lt;code&gt;subprocess.Popen(["open", "-a", app_name, filepath])&lt;/code&gt; and query available apps using &lt;code&gt;NSWorkspace&lt;/code&gt; via PyObjC.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On Windows&lt;/strong&gt;, you can use the &lt;code&gt;SHAssocEnumHandlers&lt;/code&gt; COM API or query the registry, though this is more involved.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a Linux-focused application, GIO is the natural and reliable choice. When you're ready to distribute your finished application, take a look at &lt;a href="https://www.pythonguis.com/tutorials/packaging-pyqt5-applications-linux-pyinstaller/"&gt;packaging PyQt6 apps for Linux with PyInstaller&lt;/a&gt; to create standalone executables.&lt;/p&gt;
&lt;h2 id="complete-working-example"&gt;Complete working example&lt;/h2&gt;
&lt;p&gt;Here is the full application with icons included. If you need help setting up your PyQt6 development environment first, see the &lt;a href="https://www.pythonguis.com/installation/install-pyqt6-linux/"&gt;PyQt6 installation guide for Linux&lt;/a&gt;.&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
import os
import subprocess

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (
    QApplication,
    QListWidget,
    QMainWindow,
    QMenu,
)

from gi.repository import Gio


def get_mime_type(filepath):
    """Return the MIME type string for a given file path."""
    gfile = Gio.File.new_for_path(filepath)
    info = gfile.query_info(
        "standard::content-type", Gio.FileQueryInfoFlags.NONE, None
    )
    return info.get_content_type()


def get_apps_for_mime(mime_type):
    """Return a list of Gio.AppInfo objects for the given MIME type."""
    return Gio.AppInfo.get_all_for_type(mime_type)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Open With Example")
        self.resize(500, 400)

        self.list_widget = QListWidget()
        self.setCentralWidget(self.list_widget)

        # Add files from common directories. Update these paths to
        # match your system.
        search_dirs = [
            os.path.expanduser("~/Documents"),
            os.path.expanduser("~/Pictures"),
            os.path.expanduser("~/Downloads"),
        ]

        for folder in search_dirs:
            if os.path.isdir(folder):
                for entry in sorted(os.listdir(folder)):
                    full_path = os.path.join(folder, entry)
                    if os.path.isfile(full_path):
                        self.list_widget.addItem(full_path)

        self.list_widget.setContextMenuPolicy(
            Qt.ContextMenuPolicy.CustomContextMenu
        )
        self.list_widget.customContextMenuRequested.connect(
            self.show_context_menu
        )

    def show_context_menu(self, position):
        item = self.list_widget.itemAt(position)
        if item is None:
            return

        filepath = item.text()
        menu = QMenu(self)

        # Open with default application.
        open_action = menu.addAction("Open")
        open_action.triggered.connect(
            lambda: self.open_default(filepath)
        )

        # Open containing folder.
        open_folder_action = menu.addAction("Open Containing Folder")
        open_folder_action.triggered.connect(
            lambda: self.open_containing_folder(filepath)
        )

        menu.addSeparator()

        # Open With submenu.
        open_with_menu = menu.addMenu("Open With...")
        self.populate_open_with_menu(open_with_menu, filepath)

        menu.exec(self.list_widget.mapToGlobal(position))

    def populate_open_with_menu(self, submenu, filepath):
        """Fill the 'Open With' submenu with available applications."""
        try:
            mime_type = get_mime_type(filepath)
        except Exception:
            action = submenu.addAction("(unable to detect file type)")
            action.setEnabled(False)
            return

        apps = get_apps_for_mime(mime_type)

        if not apps:
            action = submenu.addAction("(no applications found)")
            action.setEnabled(False)
            return

        for app_info in apps:
            app_name = app_info.get_display_name()
            icon = app_info.get_icon()

            action = submenu.addAction(app_name)

            # Try to set the application's icon from the system theme.
            if icon is not None and hasattr(icon, "get_names"):
                for icon_name in icon.get_names():
                    qt_icon = QIcon.fromTheme(icon_name)
                    if not qt_icon.isNull():
                        action.setIcon(qt_icon)
                        break

            action.triggered.connect(
                lambda checked, ai=app_info: self.open_with_app(
                    ai, filepath
                )
            )

    def open_default(self, filepath):
        """Open the file using the system default application."""
        subprocess.Popen(["xdg-open", filepath])

    def open_containing_folder(self, filepath):
        """Open the directory that contains the file."""
        folder = os.path.dirname(filepath)
        subprocess.Popen(["xdg-open", folder])

    def open_with_app(self, app_info, filepath):
        """Launch a specific application to open the given file."""
        gfile = Gio.File.new_for_path(filepath)
        try:
            app_info.launch([gfile], None)
        except Exception as e:
            print(f"Failed to launch {app_info.get_display_name()}: {e}")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Run this application, and you'll see a list of files from your home directories. Right-click any file and you'll get a context menu with "Open", "Open Containing Folder", and an "Open With..." submenu listing every application your system knows can handle that file type &amp;mdash; just like a native file manager.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt5 see my book, &lt;a href="https://www.martinfitzpatrick.com/pyqt5-book/"&gt;Create GUI Applications with Python &amp; Qt5.&lt;/a&gt;&lt;/p&gt;
            </content><category term="PyQt6"/><category term="QMenu"/><category term="Context-Menu"/><category term="Linux"/><category term="Desktop-Integration"/></entry><entry><title>How to Show a QMenu Title in PyQt6 — Adding visible titles to your context menus using QLabel, QWidgetAction, and other approaches</title><link href="https://www.pythonguis.com/faq/how-to-show-qmenu-title/" rel="alternate"/><published>2020-05-24T09:00:00+00:00</published><updated>2020-05-24T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2020-05-24:/faq/how-to-show-qmenu-title/</id><summary type="html">If you've ever tried to set a title on a &lt;code&gt;QMenu&lt;/code&gt; in PyQt6, you've probably noticed something frustrating: calling &lt;code&gt;setTitle()&lt;/code&gt; on a menu doesn't actually display a title on the popup itself. The &lt;code&gt;title&lt;/code&gt; property on &lt;code&gt;QMenu&lt;/code&gt; is used for something else entirely &amp;mdash; it's the text that appears when the menu is added to a &lt;code&gt;QMenuBar&lt;/code&gt; (like "File", "Edit", etc.). When you show a &lt;code&gt;QMenu&lt;/code&gt; as a standalone context menu, that title is invisible.</summary><content type="html">
            &lt;p&gt;If you've ever tried to set a title on a &lt;code&gt;QMenu&lt;/code&gt; in PyQt6, you've probably noticed something frustrating: calling &lt;code&gt;setTitle()&lt;/code&gt; on a menu doesn't actually display a title on the popup itself. The &lt;code&gt;title&lt;/code&gt; property on &lt;code&gt;QMenu&lt;/code&gt; is used for something else entirely &amp;mdash; it's the text that appears when the menu is added to a &lt;code&gt;QMenuBar&lt;/code&gt; (like "File", "Edit", etc.). When you show a &lt;code&gt;QMenu&lt;/code&gt; as a standalone context menu, that title is invisible.&lt;/p&gt;
&lt;p&gt;So how do you show a visible heading at the top of a context menu? There are a few approaches, each with their own trade-offs. In this tutorial, we'll walk through all of them and finish with the most flexible solution: embedding a &lt;code&gt;QLabel&lt;/code&gt; widget directly in the menu using &lt;code&gt;QWidgetAction&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="why-settitle-doesnt-work"&gt;Why &lt;code&gt;setTitle()&lt;/code&gt; Doesn't Work&lt;/h2&gt;
&lt;p&gt;When you create a &lt;code&gt;QMenu&lt;/code&gt; and call &lt;code&gt;setTitle()&lt;/code&gt;, you're setting the label that would appear on a menu bar. For example:&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;menu = QMenu()
menu.setTitle("My Menu")
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;If you added this menu to a &lt;code&gt;QMenuBar&lt;/code&gt;, you'd see "My Menu" as a clickable item on the bar. But when you show this menu as a popup context menu (using &lt;code&gt;.show()&lt;/code&gt; or &lt;code&gt;.exec()&lt;/code&gt;), that title is nowhere to be seen.&lt;/p&gt;
&lt;p&gt;This catches a lot of people off guard, but once you know why it works this way, you can reach for the right solution.&lt;/p&gt;
&lt;h2 id="using-addsection"&gt;Using &lt;code&gt;addSection()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Qt provides a built-in method called &lt;code&gt;addSection()&lt;/code&gt; that's designed to add labeled section headers to a menu. You might think this is exactly what you need &amp;mdash; and depending on your platform and theme, it might be.&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;menu = QMenu()
menu.addSection("My Menu Title")
menu.addAction("Option 1")
menu.addAction("Option 2")
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The catch is that &lt;code&gt;addSection()&lt;/code&gt; renders differently depending on the platform style. On macOS and Windows native themes, it often looks identical to a plain separator line &amp;mdash; the text simply doesn't show up.&lt;/p&gt;
&lt;p&gt;The one reliable way to make &lt;code&gt;addSection()&lt;/code&gt; visible is to use the &lt;strong&gt;Fusion&lt;/strong&gt; style, which is Qt's built-in cross-platform theme:&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;app = QApplication(sys.argv)
app.setStyle("Fusion")
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;With Fusion active, &lt;code&gt;addSection()&lt;/code&gt; displays the section text alongside a separator line, which looks quite nice. The downside is that Fusion doesn't match the native look and feel of any operating system, so your entire application will look slightly different from other apps on the user's desktop.&lt;/p&gt;
&lt;h2 id="using-a-disabled-action"&gt;Using a Disabled Action&lt;/h2&gt;
&lt;p&gt;A simpler workaround is to add a regular action and then disable it. A disabled action shows its text but appears grayed out and can't be clicked:&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;menu = QMenu()
title_action = menu.addAction("My Menu Title")
title_action.setDisabled(True)
menu.addSeparator()
menu.addAction("Option 1")
menu.addAction("Option 2")
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;This works on all platforms and themes. The text shows up, and users can't accidentally click on it. However, it looks like a disabled menu item, which could be confusing &amp;mdash; users might wonder what they need to do to enable it.&lt;/p&gt;
&lt;h2 id="using-a-qlabel-with-qwidgetaction"&gt;Using a QLabel with QWidgetAction&lt;/h2&gt;
&lt;p&gt;The most flexible approach is to embed a &lt;code&gt;QLabel&lt;/code&gt; widget directly into the menu using &lt;code&gt;QWidgetAction&lt;/code&gt;. This lets you fully control the appearance of the title &amp;mdash; you can center it, make it bold, change its background color, and more.&lt;/p&gt;
&lt;p&gt;Here's how it works. First, create a &lt;code&gt;QLabel&lt;/code&gt; with the text you want:&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.QtWidgets import QLabel, QWidgetAction, QSizePolicy
from PyQt6.QtCore import Qt

label = QLabel("My Menu Title")
label.setAlignment(Qt.AlignCenter)
label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Then wrap the label in a &lt;code&gt;QWidgetAction&lt;/code&gt; and add it to the menu. Note that the parent of the &lt;code&gt;QWidgetAction&lt;/code&gt; should be the menu itself (not the label) &amp;mdash; this ensures proper memory management and avoids crashes:&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;menu = QMenu()

widget_action = QWidgetAction(menu)
widget_action.setDefaultWidget(label)
menu.addAction(widget_action)

menu.addSeparator()
menu.addAction("Option 1")
menu.addAction("Option 2")
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;You can style the label however you like. For example, to make it bold with a colored background:&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;label.setStyleSheet("font-weight: bold; background-color: #e0e0e0; padding: 4px;")
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;This approach works reliably across all platforms and themes, and gives you complete control over how the title looks.&lt;/p&gt;
&lt;h2 id="complete-working-example"&gt;Complete Working Example&lt;/h2&gt;
&lt;p&gt;Here's a full example that demonstrates all three approaches side by side. Right-click anywhere in the window to see a context menu with a &lt;code&gt;QLabel&lt;/code&gt;-based title. You can uncomment the alternative approaches to compare them.&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 PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QMenu, QLabel,
    QWidgetAction, QSizePolicy
)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.setWindowTitle("QMenu Title Example")
        self.resize(400, 300)

        label = QLabel("Right-click anywhere in this window")
        label.setAlignment(Qt.AlignCenter)
        self.setCentralWidget(label)

    def contextMenuEvent(self, event):
        menu = QMenu(self)

        # --- Approach 1: QLabel with QWidgetAction (recommended) ---
        title_label = QLabel("My Menu Title")
        title_label.setAlignment(Qt.AlignCenter)
        title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        title_label.setStyleSheet(
            "font-weight: bold; padding: 4px;"
        )

        title_action = QWidgetAction(menu)
        title_action.setDefaultWidget(title_label)
        menu.addAction(title_action)

        # --- Approach 2: Disabled action (uncomment to try) ---
        # title_action = menu.addAction("My Menu Title")
        # title_action.setDisabled(True)

        # --- Approach 3: addSection (uncomment to try, works best with Fusion) ---
        # menu.addSection("My Menu Title")

        menu.addSeparator()
        menu.addAction("Select All")
        menu.addAction("Copy")
        menu.addAction("Paste")
        menu.addSeparator()
        menu.addAction("Settings...")

        menu.exec(event.globalPos())


if __name__ == "__main__":
    app = QApplication(sys.argv)

    # Uncomment the next line to try the Fusion style with addSection()
    # app.setStyle("Fusion")

    window = MainWindow()
    window.show()
    sys.exit(app.exec())
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;When you run this and right-click in the window, you'll see a context menu with a bold, centered title at the top.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;QWidgetAction&lt;/code&gt; approach is the most versatile because you have full control over the widget. You could swap the &lt;code&gt;QLabel&lt;/code&gt; for any widget &amp;mdash; even a small layout with an icon and text, if you wanted to get creative.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;title&lt;/code&gt; property on &lt;code&gt;QMenu&lt;/code&gt; is meant for menu bar display, not for context menu headings. To show a visible title on a popup menu, you have three options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;addSection()&lt;/code&gt;&lt;/strong&gt; &amp;mdash; built-in but only visible with certain styles (like Fusion).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disabled action&lt;/strong&gt; &amp;mdash; simple but looks like a grayed-out menu item.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;QWidgetAction&lt;/code&gt; with a &lt;code&gt;QLabel&lt;/code&gt;&lt;/strong&gt; &amp;mdash; looks like a heading and gives you full control over the title's appearance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Usually &lt;code&gt;QWidgetAction&lt;/code&gt; is the one you want to go with. For more on building menus, toolbars, and actions in PyQt6, see our tutorial on &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-actions-toolbars-menus/"&gt;Actions, Toolbars &amp;amp; Menus in PyQt6&lt;/a&gt;. If you're building custom widgets like styled labels or other UI elements, you may also find our guide to &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-creating-your-own-custom-widgets/"&gt;creating your own custom widgets in PyQt6&lt;/a&gt; helpful.&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.martinfitzpatrick.com/pyqt6-book/"&gt;Create GUI Applications with Python &amp; Qt6.&lt;/a&gt;&lt;/p&gt;
            </content><category term="pyqt6"/><category term="pyqt"/><category term="python"/><category term="qmenu"/><category term="context-menu"/><category term="qt"/><category term="qt6"/></entry></feed>