Removing Gaps Between Custom Widgets in PyQt6 Layouts

Use size policies and size hints to control how custom-painted widgets fit together
Heads up! You've already completed this tutorial.

I've created custom widgets using QPainter (a button and a top bar) and placed them in a layout, but there's a visible gap between them. How can I line up custom widgets in a layout without any space between them?

When you build custom widgets with QPainter and place them into a layout, Qt doesn't automatically know how big each widget should be or how it should behave when the window is resized. Without that information, the layout manager distributes extra space between your widgets — and that's where those unwanted gaps come from.

The solution involves two things: size policies and size hints. Together, these tell the layout exactly how your widgets should be sized and whether they should stretch to fill available space.

What are size policies and size hints?

Every widget in Qt has a size hint — a preferred default size — and a size policy — a rule that describes how the widget should grow or shrink relative to that preferred size.

For example:

  • A top bar should expand horizontally to fill the width of the window, but stay at a fixed height.
  • A button should stay at a fixed size in both directions.

You communicate these intentions to the layout system using QSizePolicy and by implementing the sizeHint() method on your widget class.

Setting a size hint

The sizeHint() method returns a QSize that tells the layout what your widget's ideal dimensions are. You override it in your custom widget class like this:

python
def sizeHint(self):
    return QtCore.QSize(80, 80)

This gives the layout a starting point. Without it, the layout has to guess — and it often guesses wrong.

Setting a size policy

A size policy tells the layout how flexible the widget is in each direction. You set it with setSizePolicy(). The two most useful policies here are:

  • QSizePolicy.Policy.Fixed — the widget stays exactly at its size hint.
  • QSizePolicy.Policy.Expanding — the widget grows to fill available space.

For a top bar that stretches horizontally but keeps a fixed height:

python
self.setSizePolicy(
    QSizePolicy.Policy.Expanding,
    QSizePolicy.Policy.Fixed,
)

For a button that should remain a constant size:

python
self.setSizePolicy(
    QSizePolicy.Policy.Fixed,
    QSizePolicy.Policy.Fixed,
)

When the bar is set to Expanding horizontally and the button is Fixed, the bar will grow to consume all remaining space in the layout — leaving no gap between the two widgets.

Common mistakes to avoid

There are a couple of pitfalls that can cause unexpected layout behavior with custom widgets.

Don't override self.width or self.height with plain attributes. Qt widgets have built-in methods called self.width() and self.height(). If you write self.width = 200 in your __init__, you replace the method with an integer, and other parts of Qt that call self.width() will break. Instead, use your own attribute names (like self._bar_width) or rely on sizeHint() to communicate sizes.

Don't call self.resize() inside paintEvent. Resizing a widget triggers a repaint, and if your paintEvent calls resize(), you can end up in an infinite loop. Let the layout handle sizing based on your size hint and size policy.

Putting it all together

Here's a complete working example showing a custom top bar and a custom hamburger-menu button sitting side by side with no gap. The bar expands to fill horizontal space while the button stays fixed at 80×80 pixels.

python
import sys

from PyQt6 import QtWidgets, QtCore, QtGui
from PyQt6.QtWidgets import QSizePolicy

class TopBar(QtWidgets.QWidget):
    clicked = QtCore.pyqtSignal(str)

    def __init__(self, name, parent=None):
        super().__init__(parent)
        self._name = name
        self._text_size = 30
        self.setSizePolicy(
            QSizePolicy.Policy.Expanding,
            QSizePolicy.Policy.Fixed,
        )

    def sizeHint(self):
        return QtCore.QSize(200, 80)

    def paintEvent(self, event):
        width = event.rect().width()
        height = self.height()

        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)

        # Draw background
        bar_rect = QtCore.QRect(0, 0, width, height)
        painter.fillRect(
            bar_rect, QtGui.QBrush(QtGui.QColor(160, 166, 167, 255))
        )

        # Draw text
        font = QtGui.QFont("Arial", self._text_size)
        painter.setFont(font)
        text_rect = QtCore.QRect(20, 0, width - 20, height)
        painter.drawText(
            text_rect, QtCore.Qt.AlignmentFlag.AlignVCenter, self._name
        )

        painter.end()

class CustomButton(QtWidgets.QWidget):
    clicked = QtCore.pyqtSignal(bool)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._mouse_checked = False
        self._mouse_over = False
        self._number_lines = 3
        self.setMouseTracking(True)
        self.setSizePolicy(
            QSizePolicy.Policy.Fixed,
            QSizePolicy.Policy.Fixed,
        )

    def sizeHint(self):
        return QtCore.QSize(80, 80)

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)

        # Draw background
        painter.setBrush(QtGui.QColor(160, 166, 167, 255))
        painter.setPen(QtCore.Qt.PenStyle.NoPen)
        rect = QtCore.QRect(0, 0, self.width(), self.height())
        painter.drawRect(rect)

        # Draw pressed state
        if self._mouse_checked:
            painter.setBrush(QtGui.QColor("#c00000"))
            painter.setPen(QtGui.QColor("#c00000"))
            painter.drawRect(rect)

        # Draw hamburger lines
        painter.setPen(QtGui.QColor(71, 77, 78))
        painter.setBrush(QtGui.QColor(71, 77, 78))
        for i in range(self._number_lines):
            y = int(
                (i + 1) * self.height() / ((self._number_lines - 1) * 2)
            )
            line_rect = QtCore.QRect(
                int(self.width() * 0.1), y, int(self.width() * 0.8), 5
            )
            painter.drawRect(line_rect)

        painter.end()

    def mousePressEvent(self, event):
        self._mouse_checked = True
        self.update()

    def mouseReleaseEvent(self, event):
        self._mouse_checked = False
        self.clicked.emit(True)
        self.update()

    def enterEvent(self, event):
        self._mouse_over = True

    def leaveEvent(self, event):
        self._mouse_over = False

class MainWindow(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Widgets — No Gaps")

        menu_button = CustomButton(self)
        menu_button.clicked.connect(self.menu_pressed)

        top_bar = TopBar("My Application", self)

        top_layout = QtWidgets.QHBoxLayout()
        top_layout.setSpacing(0)
        top_layout.setContentsMargins(0, 0, 0, 0)
        top_layout.addWidget(menu_button)
        top_layout.addWidget(top_bar)

        content_area = QtWidgets.QLabel("Content goes here")
        content_area.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)

        main_layout = QtWidgets.QVBoxLayout(self)
        main_layout.setSpacing(0)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.addLayout(top_layout)
        main_layout.addWidget(content_area)

    def menu_pressed(self):
        print("Menu button pressed!")

def main():
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.resize(600, 400)
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

When you run this, the button and bar sit flush against each other at the top of the window. Resize the window and the bar stretches to fill the space while the button stays the same size — exactly what you'd expect from a toolbar layout.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

PyQt/PySide Office Hours 1:1 with Martin Fitzpatrick

Save yourself time and frustration. Get one on one help with your projects. Bring issues, bugs and questions about usability to architecture and maintainability, and leave with solutions.

Book Now 60 mins ($195)

Martin Fitzpatrick

Removing Gaps Between Custom Widgets in PyQt6 Layouts was written by Martin Fitzpatrick with contributions from Leo Well.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.