0

I am just trying my first prototype in pyside (python/Qt). The application itself starts up fine, creates a window with widgets according to my layout. Threads are started and execute, all fine. Except...

I want to enhance the GUI by adding some custom widget indicating the execution state of the threads. So I thought flashing LEDs would be fine for that. For this I try to implement a custom LED widget.

Remember that I currently try to learn python, so there might be some strange approaches in this. Anyway, here is the LED widgets class in its current state:

from PySide import QtCore, QtGui

class LED(QtGui.QWidget):

    class Mode:
        STATIC_OFF   = 0
        STATIC_ON    = 1
        FLASH_SLOW   = 2
        FLASH_MEDIUM = 2
        FLASH_FAST   = 3

    class Color:
        BLACK  = '#000000'
        GREEN  = '#00FF00'
        RED    = '#FF0000'
        BLUE   = '#0000FF'
        YELLOW = '#FFFF00' 
        WHITE  = '#FFFFFF'

    mode   = Mode.STATIC_ON
    color  = Color.BLACK
    radius = 10
    status = False
    timer  = None

    outdated = QtCore.Signal()

    def __init__(self, mode, color, radius, parent=None):
        super(LED, self).__init__(parent)
        self.outdated.connect(self.update)
        self.setMode(mode,False)
        self.setColor(color,False)
        self.setRadius(radius,False)
        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.adjustAppearance)
        self.adjustAppearance()

    def getCenter(self):
        return QtCore.QPoint(self.radius, self.radius)

    def getBox(self):
        return QtCore.QRect(self.radius, self.radius)

    def setColor(self, color, update=True):
        assert color in (self.Color.GREEN,self.Color.RED,self.Color.BLUE,self.Color.YELLOW,self.Color.WHITE), "invalid color"
        self.color = color
        if update:
            self.adjustAppearance()

    def setMode(self, mode, update=True):
        assert mode in (self.Mode.STATIC_OFF,self.Mode.STATIC_ON,self.Mode.FLASH_SLOW,self.Mode.FLASH_MEDIUM,self.Mode.FLASH_FAST),"invalid mode"
        self.mode = mode
        if update:
            self.adjustAppearance()

    def setRadius(self, radius, update=True):
        assert isinstance(radius, int), "invalid radius type (integer)"
        assert 10<=radius<=100, "invalid radius value (0-100)"
        self.radius = radius
        if update:
            self.adjustAppearance()

    def switchOn(self):
        self.status = True
        self.adjustAppearance()

    def switchOff(self):
        self.status = False
        self.adjustAppearance()

    def adjustAppearance(self):
        if self.mode is self.Mode.STATIC_OFF:
            self.status = False
            self.timer.stop()
        elif self.mode is self.Mode.STATIC_ON:
            self.status = True
            self.timer.stop()
        elif self.mode is self.Mode.FLASH_SLOW:
            self.status = not self.status
            self.timer.start(200)
        elif self.mode is self.Mode.FLASH_SLOW:
            self.status = not self.status
            self.timer.start(500)
        elif self.mode is self.Mode.FLASH_SLOW:
            self.status = not self.status
            self.timer.start(1000)
            self.outdated.emit()

    def paintEvent(self, event):
        painter = QtGui.QPainter()
        painter.begin(self)
        self.drawWidget(event, painter)
        painter.end()

    def drawWidget(self, event, painter):
        if self.status:
            shade = QtGui.QColor(self.color).darker
        else:
            shade = QtGui.QColor(self.color).lighter
        #painter.setPen(QtGui.QColor('black'), 1, QtCore.Qt.SolidLine)
        painter.setPen(QtGui.QColor('black'))
        painter.setBrush(QtCore.Qt.RadialGradientPattern)
        painter.drawEllipse(self.getCenter(), self.radius, self.radius)

My problem is that the widget simply does not show when I add it to the windows layout. Other widgets (non-custome, plain Qt widgets) do show, so I gues it is not a question of creating the widget, not a question of how I use the widget. Nevertheless here is the (shortened) instanciation if the widget:

class View(QtGui.QMainWindow):

    ui = None

    def __init__(self, config, parent=None):
        log.debug("window setup")
        self.config = config
        super(View, self).__init__(parent)
        try:
            self.ui = self.Ui(self)
            self.setObjectName("appView")
            self.setWindowTitle("AvaTalk")
            self.show()
        except RuntimeError as e:
            log.error(e.message)

    class Ui(QtCore.QObject):

        # [...]
        iconDetector = None
        buttonDetector = None
        # [...]

        def __init__(self, window, parent=None):
            log.debug("ui setup")
            super(View.Ui, self).__init__(parent)
            self.window = window

            # central widget
            log.debug("widget setup")
            self.centralWidget = QtGui.QWidget()
            self.widgetLayout = QtGui.QVBoxLayout(self.centralWidget)

            # create toolbars
            #self.createMenubar()
            #self.createCanvas()
            self.createToolbar()
            #self.createStatusbar()

            # visualize widget
            self.window.setCentralWidget(self.centralWidget)

            # actions
            log.debug("actions setup")
            self.actionQuit = QtGui.QAction(self.window)
            self.actionQuit.setObjectName("actionQuit")
            self.menuFile.addAction(self.actionQuit)
            self.menubar.addAction(self.menuFile.menuAction())

            log.debug("connections setup")
            QtCore.QObject.connect(self.actionQuit, QtCore.SIGNAL("activated()"), self.window.close)
            QtCore.QMetaObject.connectSlotsByName(self.window)

        def createToolbar(self):
            log.debug("toolbar setup")
            self.toolbar = QtGui.QHBoxLayout()
            self.toolbar.setObjectName("toolbar")
            self.toolbar.addStretch(1)
            # camera
            # detector
            self.iconDetector = LED(LED.Mode.STATIC_OFF,LED.Color.GREEN,10,self.window)
            self.buttonDetector = IconButton("Detector", "detector",self.window)
            self.toolbar.addWidget(self.iconDetector)
            self.toolbar.addWidget(self.buttonDetector)
            self.toolbar.addStretch(1)
            # analyzer
            # extractor
            # layout
            self.widgetLayout.addLayout(self.toolbar)

It might well be that the actual painting using QPainter is still nonsense. I did not yet come to test that: actually when testing I find that isVisible() returns False on the widget after the setup has completed. So I assume I miss a central point. Unfortunately I am unable to find out what I miss...

Maybe someone can spot my issue? Thanks !

arkascha
  • 41,620
  • 7
  • 58
  • 90
  • I think you must call update() in paintEvent function. – Reza Ebrahimi Feb 23 '13 at 09:07
  • @Reza I doubt that... Actually `painEvent()` is never called, which is a symptom of my problem. It is usually called (I read) when `update()` is triggered. So the other way 'round. – arkascha Feb 23 '13 at 09:26
  • as I know paintEvent() called when a widget has a parent and goes to drawing, check your widget have show() function and if yes, call them in your class constructor (__init__()), hope this helps. – Reza Ebrahimi Feb 23 '13 at 10:18
  • @Reza: Calling `show()` inside the constructor does not change anything, tried that before. Also I never saw a `show()` inside a custom widget in all the examples around. And the widget _does_ have a parent, I pass the containing widget as parent (the window in this case). – arkascha Feb 23 '13 at 10:34
  • Can you paste your setup? Getting `False` from `isVisible` is odd, if this is added to a visible widget. – Avaris Feb 23 '13 at 15:39
  • @Avaris: ok, I added the (shortened) instanciation of the widget to the question. It is the second code block. That stuff is shortened quite a lot to concentrate on the relevant bits. Hope the idea gets clear: a window, inside a layout that holds a toolbar, that toolbar holds some widgets, some of those are meant to be the LED widgets I have problems with. The window shows fine, also the widgets inside (buttons), but not the LED widgets. No error, just not visible. – arkascha Feb 24 '13 at 09:50
  • @arkascha: It is important to provide a self-contained runnable example that demonstrates the issue. I can't run your setup without guessing certain things (`IconButton`, indentation error with `createToolbar`?, etc.). There is one issue with `LED`, but since I can't run your code, I can't be certain that you don't have other issues. Anyway, I'll write an answer for the issue that I'm certain. – Avaris Feb 24 '13 at 10:19
  • @Avaris: right, corrected the indentation issue, thanks for pointing that out. It is the result of pasting code using tabs for indentation here at SO where I have to manually adjust it to show up readable. I very much apprechiate your attempt! – arkascha Feb 24 '13 at 10:49

1 Answers1

2

One thing to be careful when implementing custom widgets derived from QWidget is: sizeHint or minimumSizeHint for QWidget returns invalid QSize by default. This means, if it is added to a layout, depending on the other widgets, it will shrink to 0. This effectively makes it 'not-visible'. Although, isVisible would still return True. Widget is 'visible', but it just doesn't have anything to show (0 size). So, if you're getting False, there is definitely another issue at hand.

So it is necessary to define these two methods with sensible sizes:

class LED(QtGui.QWidget):
    # ...

    def sizeHint(self):
        size = 2 * self.radius + 2
        return QtCore.QSize(size, size)

    def minimumSizeHint(self):
        size = 2 * self.radius + 2
        return QtCore.QSize(size, size)

Note: There are other issues:

  • Like defining mode, color, etc as class attributes and then overriding them with instance attributes. They won't break anything but they are pointless.
  • painter.setBrush(QtCore.Qt.RadialGradientPattern) is wrong. You can't create a brush with QtCore.Qt.RadialGradientPattern. It is there, so that brush.style() can return something. If you want a gradient pattern you should create a brush with QGradient constructor.
  • if self.mode is self.Mode.STATIC_OFF: comparing with is is wrong. is compares identity, you want == here. (also, FLASH_SLOW and FLASH_MEDIUM are both 2)
  • assert is for debugging and unit tests. You shouldn't use it in real code. Raise an exception if you want. Or have sensible defaults, where invalid values would be replaced with that.
Avaris
  • 35,883
  • 7
  • 81
  • 72
  • Ok, I will implement those methods. Although I am surprised: all the examples I found on the internet come _without_ these methods... – arkascha Feb 24 '13 at 10:50
  • Additional points 1 and 3 are the result of me being a bloody python newbie. Thanks for pointing that stuff out, I will dig into it. Point 4 surprises me, but ok. I thought assert _does_ raise an exception and is just a shortcut for a conditional with explicit exception raising. Maybe I understood the documentation wrong. Maybe point 2 is my issue here, although I am surprised that I do not get a warning or error. I will look into it, thanks. But before all I will invest time in a stripped down wrapper that uses my classes. I will post that example. – arkascha Feb 24 '13 at 10:54
  • @arkascha: Regarding those examples, it depends. If the widget is intended to be a window, it doesn't need those methods. If it's a subwidget in a layout, it _should_ implement those methods. I can't say more without seeing the examples. As for `assert`, it does raise an exception. But it is usually used in unit tests. If you want to raise an exception, you should raise an appropriate one (like `ValueError`). `assert` in regular code is generally, bad practice. Also python has `-O` (optimization) flag which [will skip `assert`s](http://stackoverflow.com/a/4777156/843822). – Avaris Feb 24 '13 at 11:05
  • I made a stripped down version: https://cloud.christian-reiner.info/public.php?service=files&t=9190e830b594e9752b144beb9cc8df46 I know it is not fine not to post an example, but that is simply too much code... Even after I removed all classes and stuff not required for this test. – arkascha Feb 24 '13 at 11:09
  • @arkascha: #2 creates a warning here: `QBrush: Wrong use of a gradient pattern`. And, it doesn't work (no gradient), which is not surprising actually, since gradients require additional parameters. – Avaris Feb 24 '13 at 11:10
  • @arkascha: Well, #2 creates the warning _after_ `*Hint` methods. Before it's simply a `0` size widget and it doesn't have anything to paint. So `paintEvent` (including that line) is not executed. – Avaris Feb 24 '13 at 11:18
  • Ok, that Brush initialization is a test result. Originally it was called using `shade` which is defined right above as a QColor. I reverted it back. Still no visible icon. However I did not see any warning before, so maybe I miss something there... What did you mean by "after `*Hint` methods"? Ah, wait, probably the two methods you mentioned... – arkascha Feb 24 '13 at 11:18
  • Ah!! After adding those `*Hint` methods at least `painEvent()` is executed! Throws warnings and errors, but is executed! That is great! – arkascha Feb 24 '13 at 11:23
  • Bringo! Adding the two `*Hint` methods and correcting the QBrush initialization finally shows the icons! Ugly as hell, but that's fine cause it is a starting point. And its a quick-n-dirty prototype anyway... So: thanks for the great help! – arkascha Feb 24 '13 at 11:31
  • @arkascha: Glad it helped. Have fun :). – Avaris Feb 24 '13 at 11:38