Search code examples
pythonqtpyqtpyqt5qtgui

Qt Custom CheckBox, help about PaintEvent


I am trying to create a custom checkbox. I am using paintEvent function to create my special checkbox. It's design:

Design

The result on Qt:

Qt

First of all, rounded should be added and the junction of the lines should be a smoother transition. I need a more professional solution. Which is pretty looking. Thanks! Code:

import sys, os, time
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
    
class ECheckBoxData(object):
    Radius = 10
    AnimationTime = 600  # ms
    FontSize, FontSpacing = 16, 0
    Color = {
        "CORNER": QColor(239, 239, 239),
        "BASE_BACKGROUND": QColor(255, 125, 51),
        "BASE_FOREGROUND": QColor(255, 152, 91),

        "BASE_HOVER_BACKGROUND" :QColor(255, 152, 91),
        "BASE_HOVER_FOREGROUND": QColor(247, 247, 250),

    }
    TextElide = Qt.ElideMiddle
    CheckWidth, CheckHeight = 128, 128    


class ECheckBox(QCheckBox):
    CheckBoxData = ECheckBoxData()
   
    def __init__(self, CheckBoxData=ECheckBoxData()):
        super(ECheckBox, self).__init__(None)
        self.CheckBoxData = CheckBoxData
            
        self.myfont = QFont("Times New Roman", 16, weight=QFont.Bold)
        self.myfont.setWordSpacing(self.CheckBoxData.FontSpacing)
        self.myfont.setStyleHint(QFont.Monospace)
        self.myfontMetrics = QFontMetrics(self.myfont)
        # font.setStyleHint(QFont.Times, QFont.PreferAntialias)
        self.setFont(self.myfont)
        self.setFixedHeight(self.CheckBoxData.CheckHeight+2)
        self.setMinimumWidth(self.CheckBoxData.CheckWidth+8)
       
    def paintEvent(self, event: QPaintEvent):
        pt = QPainter(self)
        pt.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing )
        border = QPainterPath()
        pt.setBrush(self.CheckBoxData.Color["BASE_BACKGROUND"])
        pt.setPen(QPen(self.CheckBoxData.Color["CORNER"],5))
        border.addRoundedRect(QRectF(2,2,self.CheckBoxData.CheckWidth-2, self.CheckBoxData.CheckHeight-2),self.CheckBoxData.Radius, self.CheckBoxData.Radius)

        pt.drawPath(border)
        pt.setClipPath(border)

        pt.setPen(QPen(Qt.white,self.CheckBoxData.CheckWidth/10))
        pt.setBrush(Qt.white)
        path2 = QPainterPath()
        arrow_width, arrow_height = self.width()/4, self.height()/ (66/8)
        center_width, center_height = int(self.width()/2), int(self.height()/2)

        #path2.moveTo((self.width() - arrow_width * 2) / 2, (center_height + 2))
        #path2.lineTo(QPoint((self.width() - arrow_width) / 2 + 2, (center_height) + arrow_height + 1))
        #path2.lineTo(QPoint((self.width()-arrow_width), (center_height)-arrow_height))
        path2.addPolygon(QPolygonF([
            QPoint((self.width()-arrow_width*2)/2, (center_height+2)), QPoint((self.width()-arrow_width)/2+2, (center_height)+arrow_height+1)
        ]))
        path2.addPolygon(QPolygonF([QPoint((self.width()-arrow_width)/2+2, (center_height)+arrow_height+1), QPoint((self.width()-arrow_width-12), (center_height)-arrow_height)]))
        
        pt.drawPath(path2)
          


if __name__ == "__main__":
    app = QApplication(sys.argv)
    wind = QMainWindow()
    wind.setStyleSheet("QMainWindow{background-color:rgb(247,247,250)}")
    wind.resize(221, 150)
    wid = QWidget()
    lay = QHBoxLayout(wid)
    lay.setAlignment(Qt.AlignCenter)    
    Data = ECheckBoxData()
    e = ECheckBox(Data)
    e.setChecked(True)    
    lay.addWidget(e)
    wind.setCentralWidget(wid)
    wind.show()
    sys.exit(app.exec())

Solution

  • In order to create a smooth and curved outline, you need to properly set the QPen cap and join style.

    Using a polygon to draw the outline is obviously not a valid solution, as that outline will be drawn with the pen, but what you need is a path that will be painted with a thick pen and the preferred cap and join styles.

    Also, in order to be able to draw a good icon at different sizes, you should not rely on fixed sizes (even if properly computed), but use the current size as a reference instead.

        def paintEvent(self, event: QPaintEvent):
            pt = QPainter(self)
            pt.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
    
            size = min(self.width(), self.height())
            border = max(1, size / 32)
            rect = QRectF(0, 0, size - border, size - border)
            # move the square to the *exact* center using a QRectF based on the
            # current widget; note: it is very important that you get the center
            # using a QRectF, because the center of QRect is always in integer
            # values, and will almost always be imprecise at small sizes
            rect.moveCenter(QRectF(self.rect()).center())
    
            borderPath = QPainterPath()
            # with RelativeSize we can specify the radius as 30% of half the size
            borderPath.addRoundedRect(rect, 30, 30, Qt.RelativeSize)
    
            pt.setBrush(self.CheckBoxData.Color["BASE_BACKGROUND"])
            pt.setPen(QPen(self.CheckBoxData.Color["CORNER"], border * 2.5))
            pt.drawPath(borderPath)
    
            pt.setPen(QPen(Qt.white, size * .125, 
                cap=Qt.RoundCap, join=Qt.RoundJoin))
            arrow_path = QPainterPath()
            arrow_path.moveTo(size * .25, size * .5)
            arrow_path.lineTo(size * .40, size * .65)
            arrow_path.lineTo(size * .7, size * .325)
            pt.drawPath(arrow_path.translated(rect.topLeft()))