Custom widgets without a gap between each widget

Heads up! You've already completed this tutorial.

Pier-Justin_Mora | 2021-04-16 14:49:24 UTC | #1

Hello guys I have created a custom button and a custom bar. Both are created in a separate class. Because I want to extend it and use it like a construction plan by putting all the parts together. There is only one problem. This is also represented in the following image. The custom widgets should be lined up without a gap. How can I make it so that there is no space between the widgets?

customWidget|690x387

Here the code:

python
from PyQt5 import QtWidgets, QtCore, QtGui, Qt

def clear(layout):
        if layout is not None:
            while layout.count():
                child = layout.takeAt(0)
                print(child)
                if child.widget() is not None:
                    child.widget().setParent(None)
                    # child.widget().deleteLater() ##also deletes the content of the file
                if child.layout() is not None:
                    # child.layout().setParent(None)
                    clear(child.layout())

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

    def __init__(self, name, icon_path, width, height, parent=None, **kwargs):
        # super().__init__()
        super(TopBar, self).__init__(parent)
        self.__color = QtGui.QColor("#215dde")
        self.__name = name
        self.__icon_path = icon_path
        self.action_lst = []
        self.__sig_lst = []
        self.actual_action = None
        self.__min_size = False
        self.text_size = 30
        self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.FramelessWindowHint)
        # self.setSizePolicy(
        #     # policy
        #     QSizePolicy.Fixed,
        #     QSizePolicy.Fixed
        #     # QSizePolicy.Maximum
        # )
        self.width = width
        self.height = height
        self.resize(self.width, self.height)


    def __del__(self):
        print('Object deleted.')

    def paintEvent(self, event):
        self.width = event.rect().width()
        painter = QtGui.QPainter()
        painter.begin(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        fontText = QtGui.QFont(painter.font())
        fontText.setFamily("Arial")
        fontText.setPixelSize(self.text_size)
        painter.setFont(fontText)
        actual_width = 0
        bar_rect = QtCore.QRect(0, 0, self.width, self.height)
        painter.fillRect(bar_rect, QtGui.QBrush(QtGui.QColor(160, 166, 167, 255)))


        image = QtGui.QPixmap(self.__icon_path)
        text_height = self.height
        icon_rect = QtCore.QRect(text_height*0.1, text_height*0.1, text_height*0.9, text_height*0.9)
        painter.drawPixmap(icon_rect,image)
        actual_width = text_height*0.9


        size_text_line = QtCore.QSize(painter.fontMetrics().size(QtCore.Qt.TextSingleLine, self.__name))
        text_rect = QtCore.QRect(QtCore.QPoint(actual_width + text_height*1 , text_height-size_text_line.height()), size_text_line)
        painter.drawText(text_rect, QtCore.Qt.AlignBottom, self.__name)

        painter.end()


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

    def __init__(self, icon_path, parent=None, **kwargs):
        # super().__init__()
        super(Custom_Button, self).__init__(parent)
        self.__color = QtGui.QColor("#215dde")
        self.setMouseTracking(True)
        self.__mouse_checked = False
        self.__mouse_over = False
        self.actual_action = None
        self.__pixmap = QtGui.QPixmap(icon_path)
        self.number_lines = 3
        self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.FramelessWindowHint)
        self.setMouseTracking(True)

    def sizeHint(self):
        return QtCore.QSize(150,100)

    def setText(self, text):
        self.__text = text

    def __del__(self):
        print('Button deleted.')


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

        self.button_width = 80
        self.button_height = 80

        self.resize(self.button_width, self.button_height)

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

        if(self.__mouse_checked):
            painter.setBrush(QtGui.QColor("#c00000"))
            painter.setPen(QtGui.QColor("#c00000"))
            painter.drawRect(QtCore.QRect(0,0,self.width(), self.height()))

        for i in range(self.number_lines):
            actual_height = (i+1) * self.height() / ((self.number_lines - 1)*2)
            line = QtCore.QRect(self.width() *0.1 , actual_height, self.width() *0.8, 5)##QRect(QPoint(self.painter.device().width()*0.3 , actual_height), size_text_line)###QRect because to color the whole line not only the text or the area of the text
            painter.setPen(QtGui.QColor(71, 77, 78))
            painter.setBrush(QtGui.QColor(71, 77, 78))
            painter.drawRect(line)##drawLine(line)
        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 mouseMoveEvent(self, event):
        self.__mouse_over = True


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


class MainWindow(QtWidgets.QWidget):
    def __init__(self, parent=None, **kwargs):
        super(MainWindow, self).__init__(parent)
    # def __init__(self):
        super().__init__()
        self.ui()


    def ui(self):
        self.widgets()
        self.actions()
        self.layout()


    def widgets(self):
        self.__top_bar = TopBar("Text", "Icons/dots.png", self.width(), 80, self)
        self.__menu_button = Custom_Button("Icons/menu.png", self)
        self.__actual_widget = QtWidgets.QFrame()
        self.__first_widget = QtWidgets.QFrame()

    def actions(self):
        self.__menu_button.clicked.connect(self.menu_Pressed)

    def menu_Pressed(self):
        print("Pressed!!!")


    def Back(self):
        if(self.__last == "Start" or self.__last == "Calibration"):
            self.new_Content(self.__first_widget)
            self.new_Menu(self.__first_menu)
        else:
            self.menu_Action(self.__last)


    def new_Content(self, widget):
        if(self.__actual_widget != None):
            del self.__actual_widget
        self.__actual_widget=widget
        clear(self.__content_layout)
        self.__content_layout.addWidget(self.__actual_widget)


    def layout(self):
        self.__main_layout = QtWidgets.QVBoxLayout(self)
        self.__main_layout.setSpacing(0)
        self.__main_layout.setContentsMargins(0,0,0,0)

        self.__top_layout = QtWidgets.QHBoxLayout(self)
        self.__top_layout.setSpacing(0)
        self.__top_layout.setContentsMargins(0,0,0,0)


        self.__content_layout = QtWidgets.QHBoxLayout()
        self.__content_layout.addWidget(QtWidgets.QLabel(""))
        self.__actual_widget.setLayout(self.__content_layout)
        self.__first_widget = self.__actual_widget

        self.__top_layout.addWidget(self.__menu_button, 0, QtCore.Qt.AlignTop)
        self.__top_layout.addWidget(self.__top_bar)#, 0, QtCore.Qt.AlignTop)#,  alignment=QtCore.Qt.AlignLeft)
        self.__top_layout.setContentsMargins(0,0,0,0)
        self.__top_layout.setSpacing(0)
        # self.__top_layout.addStretch()
        # self.__top_layout.

        self.__main_layout.addLayout(self.__top_layout)
        self.__main_layout.addWidget(self.__actual_widget)

        self.setLayout(self.__main_layout)







def main():
    app = QtWidgets.QApplication([])
    volume = MainWindow()
    volume.show()
    app.exec_()

if __name__ == '__main__':
    main()

martin | 2021-04-23 17:27:40 UTC | #2

Hey @Pier-Justin_Mora welcome to the forum!

To change how the resizing of the widgets works you need to use size policies -- they tell Qt how the widget should scale to fill space around it (it at all) in layouts.

What you're asking for on the layout is for it to expand horizontally, while remaining the same height vertically. That can be achieved with.

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

In contrast the button needs to retain a fixed position at all times. You can do this with

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

In both cases, you need to specify a base size for the widget for these instructions to make any sense. To do this you can implement a sizeHint method on your classes, which returns the base size for the widgets, e.g.

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

If you set this size on the button, that will be the exact fixed size of the button. If you set this size on the bar, the height will be the exact fixed height of the bar, while the width will be the minimum width (from which the bar will expand upwards).

There are a few other errors in the code, for example you are assigning to self.width (& height) which overrides the Qt self.width() method present on all widgets. You're also calling .resize() in the paint method -- I don't know what the intent of that is, but it's a bad idea. If that call does resize the button, it will trigger a re-draw.

Making these changes, the code works (alignment needs some work)

Screenshot 2021-04-16 170619|690x131

The modified code of the two classes is copied below.

python

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

    def __init__(self, name, icon_path, width, height, parent=None, **kwargs):
        # super().__init__()
        super(TopBar, self).__init__(parent)
        self.__color = QtGui.QColor("#215dde")
        self.__name = name
        self.__icon_path = icon_path
        self.action_lst = []
        self.__sig_lst = []
        self.actual_action = None
        self.__min_size = False
        self.text_size = 30
        self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.FramelessWindowHint)
        self.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Fixed)

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

    def __del__(self):
        print('Object deleted.')

    def paintEvent(self, event):
        width = event.rect().width()
        painter = QtGui.QPainter(self)
        painter.begin(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        fontText = QtGui.QFont(painter.font())
        fontText.setFamily("Arial")
        fontText.setPixelSize(self.text_size)
        painter.setFont(fontText)
        actual_width = 0
        bar_rect = QtCore.QRect(0, 0, width, self.height())
        painter.fillRect(bar_rect, QtGui.QBrush(QtGui.QColor(160, 166, 167, 255)))


        image = QtGui.QPixmap(self.__icon_path)
        text_height = self.height()
        icon_rect = QtCore.QRect(text_height*0.1, text_height*0.1, text_height*0.9, text_height*0.9)
        painter.drawPixmap(icon_rect,image)
        actual_width = text_height*0.9


        size_text_line = QtCore.QSize(painter.fontMetrics().size(QtCore.Qt.TextSingleLine, self.__name))
        text_rect = QtCore.QRect(QtCore.QPoint(actual_width + text_height*1 , text_height-size_text_line.height()), size_text_line)
        painter.drawText(text_rect, QtCore.Qt.AlignBottom, self.__name)

        painter.end()


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

    def __init__(self, icon_path, parent=None, **kwargs):
        # super().__init__()
        super(Custom_Button, self).__init__(parent)
        self.__color = QtGui.QColor("#215dde")
        self.setMouseTracking(True)
        self.__mouse_checked = False
        self.__mouse_over = False
        self.actual_action = None
        self.__pixmap = QtGui.QPixmap(icon_path)
        self.number_lines = 3
        self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.FramelessWindowHint)
        self.setMouseTracking(True)
        self.setSizePolicy(Qt.QSizePolicy.Fixed, Qt.QSizePolicy.Fixed)

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

    def setText(self, text):
        self.__text = text

    def __del__(self):
        print('Button deleted.')


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

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

        if(self.__mouse_checked):
            painter.setBrush(QtGui.QColor("#c00000"))
            painter.setPen(QtGui.QColor("#c00000"))
            painter.drawRect(QtCore.QRect(0,0,self.width(), self.height()))

        for i in range(self.number_lines):
            actual_height = (i+1) * self.height() / ((self.number_lines - 1)*2)
            line = QtCore.QRect(self.width() *0.1 , actual_height, self.width() *0.8, 5)##QRect(QPoint(self.painter.device().width()*0.3 , actual_height), size_text_line)###QRect because to color the whole line not only the text or the area of the text
            painter.setPen(QtGui.QColor(71, 77, 78))
            painter.setBrush(QtGui.QColor(71, 77, 78))
            painter.drawRect(line)##drawLine(line)
        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 mouseMoveEvent(self, event):
        self.__mouse_over = True


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

Pier-Justin_Mora | 2021-04-23 17:28:40 UTC | #3

Hi @martin,

Thank you very much for your quick and detailed answer. You helped me a lot.


The complete guide to packaging Python GUI applications with PyInstaller.
[[ discount.discount_pc ]]% OFF for the next [[ discount.duration ]] [[discount.description ]] with the code [[ discount.coupon_code ]]

Purchasing Power Parity

Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]
Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Custom widgets without a gap between each widget was written by Martin Fitzpatrick .

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.