pythonpython-3.xqt-designerpyside2paintevent

PySide2 paint on widget created by designer


This issue is with VS Code on Win10 and Python 3.6.6. I'm new both to Python and PySide2.

I've read a lot of topics on this here at StackOverflow and possibly this is a duplicate of another topic, but I'm not able to get my widget painted.

I understand that the paintEvent() of the widget object have to be overridden somehow. Most of the examples out there does some painting on the main window but I'm not able to transfer this on widgets from an ui.file.

I've created two classes in my .py-file, MainForm and Drawer. The MainForm contains implementation of UI and I'm trying to get a widget (named "widget") painted. In my .ui-file there's a widget and a graphicsview. I'm trying to implement painting on the widget.

paintEventTest.py file looks like this:

import sys

from PySide2 import QtWidgets 
from PySide2 import QtGui
from PySide2 import QtCore
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import (
    QApplication, QPushButton, QLineEdit, QTextEdit, QSpinBox, QMainWindow, QDesktopWidget, QTableWidget, 
    QTableWidgetItem, QToolButton, QToolTip)
from PySide2.QtCore import QFile, QObject, Qt


class MainForm(QMainWindow):

    def __init__(self, ui_file, parent=None):
        super(MainForm, self).__init__(parent)
        ui_file = QFile(ui_file)
        ui_file.open(QFile.ReadOnly)


        ### Load UI file from Designer ###
        loader = QUiLoader()
        self.ui_window = loader.load(ui_file)
        ui_file.close()

        self.ui_window.show()

        ### THIS IS NOT WORKING (OBVIOUSLY?) ###

        widget = self.ui_window.widget
        drawer = Drawer()
        drawer.paintEvent(widget)



class Drawer(QtWidgets.QWidget):

    def paintEvent(self, e):
        '''
        the method paintEvent() is called automatically
        the QPainter class does all the low-level drawing
        coded between its methods begin() and end()
        '''

        qp = QtGui.QPainter()
        qp.begin(self)
        self.drawGeometry(qp)
        qp.end()

    def drawGeometry(self, qp):
        qp = QtGui.QPainter(self)
        qp.setPen(QtGui.QPen(Qt.green, 8, Qt.DashLine))
        qp.drawEllipse(40, 40, 400, 400)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    form = MainForm('./UI designer/testUI.ui')
    sys.exit(app.exec_())

testUI.ui looks like this and it's implemented in a folder "UI designer":

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>731</width>
    <height>633</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QGraphicsView" name="graphicsView">
      <property name="minimumSize">
       <size>
        <width>0</width>
        <height>200</height>
       </size>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QWidget" name="widget" native="true">
      <property name="minimumSize">
       <size>
        <width>0</width>
        <height>250</height>
       </size>
      </property>
      <property name="maximumSize">
       <size>
        <width>16777215</width>
        <height>300</height>
       </size>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>731</width>
     <height>21</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

I'm getting this with code above. I'm not expecting it to work, but have really no idea on how to reference the specific widget to paint on.

QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setPen: Painter not active
QPainter::end: Painter not active, aborted


I'm also interested in equivalent code painting on the graphicsview with graphicsscene and graphicsitem.


Solution

  • As you point out, the paintEvent should only overridden. So one option is to promote the widget, you can see several examples in these answers:

    You must have the following structure:

    ├── main.py
    ├── mywidget.py
    └── UI designer
        └── testUI.ui
    

    In the mywidget.py file, implement the class you require:

    mywidget.py

    from PySide2 import QtCore, QtGui, QtWidgets
    
    
    class Drawer(QtWidgets.QWidget):
        def paintEvent(self, e):
            """
            the method paintEvent() is called automatically
            the QPainter class does all the low-level drawing
            coded between its methods begin() and end()
            """
            qp = QtGui.QPainter()
            qp.begin(self)
            self.drawGeometry(qp)
            qp.end()
    
        def drawGeometry(self, qp):
            qp.setPen(QtGui.QPen(QtCore.Qt.green, 8, QtCore.Qt.DashLine))
            qp.drawEllipse(40, 40, 400, 400)
    

    Then you have to open your .ui with Qt Designer, right click on the widget and select promote To... in the contextual menu, then fill in the dialog with the following:

    enter image description here

    press the add button and then the promote button generating the following .ui file

    <?xml version="1.0" encoding="UTF-8"?>
    <ui version="4.0">
     <class>MainWindow</class>
     <widget class="QMainWindow" name="MainWindow">
      <property name="geometry">
       <rect>
        <x>0</x>
        <y>0</y>
        <width>731</width>
        <height>633</height>
       </rect>
      </property>
      <property name="windowTitle">
       <string>MainWindow</string>
      </property>
      <widget class="QWidget" name="centralwidget">
       <layout class="QVBoxLayout" name="verticalLayout">
        <item>
         <widget class="QGraphicsView" name="graphicsView">
          <property name="minimumSize">
           <size>
            <width>0</width>
            <height>200</height>
           </size>
          </property>
         </widget>
        </item>
        <item>
         <widget class="Drawer" name="widget" native="true">
          <property name="minimumSize">
           <size>
            <width>0</width>
            <height>250</height>
           </size>
          </property>
          <property name="maximumSize">
           <size>
            <width>16777215</width>
            <height>300</height>
           </size>
          </property>
         </widget>
        </item>
       </layout>
      </widget>
      <widget class="QMenuBar" name="menubar">
       <property name="geometry">
        <rect>
         <x>0</x>
         <y>0</y>
         <width>731</width>
         <height>23</height>
        </rect>
       </property>
      </widget>
      <widget class="QStatusBar" name="statusbar"/>
     </widget>
     <customwidgets>
      <customwidget>
       <class>Drawer</class>
       <extends>QWidget</extends>
       <header>mywidget</header>
       <container>1</container>
      </customwidget>
     </customwidgets>
     <resources/>
     <connections/>
    </ui>
    

    On the other the QUiLoader only loads the widgets that Qt provides by default so if you want to use new widget you must overwrite the createWidget method:

    main.py

    import os
    import sys
    from PySide2 import QtCore, QtGui, QtWidgets, QtUiTools
    
    from mywidget import Drawer
    
    
    class UiLoader(QtUiTools.QUiLoader):
        def createWidget(self, className, parent=None, name=""):
            if className == "Drawer":
                widget = Drawer(parent)
                widget.setObjectName(name)
                return widget
            return super(UiLoader, self).createWidget(className, parent, name)
    
    
    class MainForm(QtCore.QObject):
        def __init__(self, ui_file, parent=None):
            super(MainForm, self).__init__(parent)
            ui_file = QtCore.QFile(ui_file)
            ui_file.open(QtCore.QFile.ReadOnly)
    
            ### Load UI file from Designer ###
            loader = UiLoader()
            self.ui_window = loader.load(ui_file)
            ui_file.close()
            self.ui_window.show()
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        app.setStyle("Fusion")
        file = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), "./UI designer/testUI.ui"
        )
        form = MainForm(file)
        sys.exit(app.exec_())