GUI Development Tips and Tricks

289 views
Skip to first unread message

Marcus Ottosson

unread,
Jun 7, 2014, 4:10:59 PM6/7/14
to python_in...@googlegroups.com

Hi all,

Thought I’d share some habits I’ve grown to take for granted over the years, and encourage you to share yours, with the goal to make us all better developers. :)

1. Syntactically Awesome Stylesheets

For starters, let me just say that if you aren’t styling via CSS, you’re missing out. Python is amazing at many things but styling is not of them, and compared to CSS it’s a nightmare. I still remember the day when I styled via overriding paintEvent, taming QStyledItemDelegate to my will, abstracting away, trying to make things simpler because clearly, there had to be a better way. This is that better way.

I should also mention that though CSS will blow the minds of those who haven’t given it a shot, CSS is applicable outside of Python too, which means that the usergroup you’ll get when googling go beyond Python into Web; which is a huge resource when it comes to creating pretty UIs.

Taken further, the next incarnation of Qt GUI creation, QML, is also heavily based on the same paradigm (i.e. “declarative” as opposed to “imperative”. Google it) which means you’ll future proof your knowledge by wrapping your head around the differences.

In short, styling and declarative languages make a much better couple than styling and imperative languages.

Now, onwards!

The problem

Assuming you’re already convinced CSS is for you and your software, here is a problem you might encounter along the way - CSS often contain lots of duplicate code.

As CSS is a much less intelligent “language” with much less syntactic sugar, you’ll inevitably end up with lots of duplicated keywords and whole stacks of boiler-plate copy/paste material.

Sass is a superset of CSS, a code-generator, essentially filling in for what CSS is missing; nesting, variable definitions and encapsulation, and most importantly, a nicer syntax, one that actually cares about the visual layout of your code; just like Python. Behold:

/*Regular CSS*/
QScrollBar:vertical {
  background-color: transparent;
  border-top: none;
  margin: 0px; }

QScrollBar:horizontal {
  background-color: transparent;
  border-left: none;
  border-right: none;
  margin: 0px; }

QScrollBar:horizontal::handle {
  margin: 3px;
  border-radius: 4px;
  background: hsva(0, 0, 100%, 10%);
  min-height: 20px;
  min-width: 20px; }

QScrollBar:horizontal::add-line {
  background: white;
  width: 0px;
  height: 0px;
  subcontrol-position: bottom;
  subcontrol-origin: margin; }

QScrollBar:horizontal::sub-line {
  background: white;
  height: 0px;
  width: 0px;
  subcontrol-position: top;
  subcontrol-origin: margin; }

QScrollBar:horizontal::add-page, QScrollBar:horizontal::sub-page {
  background: none; }
// Sass
QScrollBar
    &:vertical
        background-color: transparent
        border-top: none
        margin: 0px

    &:horizontal
        background-color: transparent
        border-left: none
        border-right: none
        margin: 0px

        &::handle
            margin: 3px
            border-radius: 4px
            background: hsva(0, 0, 100%, 10%)
            min-height: 20px
            min-width: 20px

        @mixin line-mixin
            background: white
            width: 0px
            height: 0px
            subcontrol-position: bottom
            subcontrol-origin: margin

        &::add-line
            @include line-mixin

        &::sub-line
            @include line-mixin

        &::add-page, &::sub-page
            background: none

That warm feeling in your stomach right now is called “love”. But that’s nothing.


// General colors
$dark-color:      rgb(25, 25, 25)
$mid-color:       rgb(50, 50, 50)
$bright-color:    rgb(75, 75, 75)
$dimmed-color:    rgb(210, 210, 210)
$lowkey-color:    rgb(190, 190, 190)

// Special purpuse colors
$red-color:       hsl(0, 50%, 50%)
$highlight-color: hsl(35%, 50%, 80%)

// Encapsulation of re-usable sets of properties

@mixin outset
    // Apply an overall soft outwards border
    border-style: solid
    border-width: 1px
    border-left-color: lighten($bright-color, 5%)
    border-right-color: darken($bright-color, 5%)
    border-top-color: lighten($bright-color, 5%)
    border-bottom-color: darken($bright-color, 5%)

@mixin inset
    // Apply an overall soft inwards border
    border-style: solid
    border-width: 1px
    border-left-color: darken($mid-color, 5%)
    border-right-color: lighten($mid-color, 20%)
    border-top-color: darken($mid-color, 5%)
    border-bottom-color: lighten($mid-color, 20%)

// Class assignments

Body
    @include outset
    background-color: $dimmed-color

Footer
    @include outset
    background-color: $lowkey-color

Header
    @include inset
    &:hover
        color: hsva(0, 0, 100%, 80%)
        padding: -2px
        border: 2px dashed hsva(0, 0, 100%, 10%)

Process in Sublime Text

There are a number of ways you can get this hooked up with your sublime, here’s my process:

stylesheet.scss --> stylesheet.css
  1. Edit .sass
  2. F7 compiles into .css
  3. Good to go.

Initially, you might be tempted to

  1. Edit your .sass
  2. Head over to a terminal
  3. Run the sass compiler
  4. Manually specify an output file
  5. Good to go

For my purposes, their names remain identical apart from their extension and in this way I’m never reminded of the fact that I’m generating code.

Finally, since we’re talking Sublime specifically, here’s a potential build script you might want to hack away with:

SASS.sublime-build

{
    "cmd": ["sass", "$file:${file_path}/${file_base_name}.css", "--stop-on-error", "--no-cache"],
    "selector": "source.sass, source.scss, source.css",
    "line_regex": "Line ([0-9]+):",
    "shell": true
}

All roses

It isn’t all roses of course.

No gradients

Some operators are unique to Qt, like qlineargradient, and these won’t work. I have yet to find a need for them, but this is the case.

Querying properties set in CSS via Python

This applies to working with CSS in general throughout Qt. The question you’ll be faced with is “When is my CSS applied, and when can I count on values returned from querying its properties?” If this is you, let me know and I’ll show you the way.

Animation

CSS has animation capabilities, but these are unfortunately not supported by Qt. Hint: This is where QML comes in.

Oki, your turn.

Best,
Marcus

--
Marcus Ottosson
konstr...@gmail.com

Justin Israel

unread,
Jun 7, 2014, 5:19:00 PM6/7/14
to python_in...@googlegroups.com
Nice write up. I've uses SASS only once before, during an experience with writing an ipad-based front end app in the Sencha Framework. I don't think I fully experienced its features, as it was just a part of the workflow. I will have to try it out with Qt development. 
Here has been my experience using CSS + Qt4:
  • I've done static application themes using either a style.css that I read into the QApplication, or as a string literal in a module. This sets the overall theme.

  • For custom composed widgets, I have done local stylesheets that overload the application theme

  • For dynamic style behavior, I have used small template string snippets that I can swap in and out of specific components of a widget:
    style1 = "{someProperty: 1; someProperty2: 1; color: $(stateColor)s}"
    style2 = "{someProperty: 2; someProperty2: 2; color: $(stateColor)s}"
    ...
    if condition: widget.setStyleSheet(style2 % context)
    
  • I still have plenty of need to use custom paint/paintEvent approaches when I need very custom designed elements. CSS only provides the predefined states that can be defined for a given widget and doesn't really have much in the way of logic.

  • I do use gradients in my stylesheets and my paint methods

  • A downside of stylesheets is that you can't really query their properties programmatically in any easy way (as opposed to the less flexible palette system). Like, you can't really know the color of a given parent when making a color decision dynamically on a child (Example)

  • They supposedly can be slower to use and QPalette since they have to be parsed and rendered. I have hit this before in certain situations, but it isn't always a problem. Just something to be aware of. Depends on how many things in your view are being rendered via css
One thing I would love if it existed is a Qt Style Factory application. It would have a kitchen-sink style interface where mostly all combinations of widgets are laid out, and you would have an interface for setting application styles and per-widget styles that update dynamically, so that you can see and build styling quickly. I know you could kinda do something using Qt Designer but it would be really clunky and not really feature rich. My idea of an ideal interface would also help you easily construct a color palette for the app. Just brainstorming, but I think it could probably be made easier to develop CSS-based styling for Qt apps, being that it does have Qt-only CSS concepts that have to be accounted for. 

-- justin



--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/CAFRtmOAqCYvwEeBS7516cX5QeknGhvPtwZbTuR1uoL7CHAwZaw%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Justin Israel

unread,
Jun 8, 2014, 1:09:37 AM6/8/14
to python_in...@googlegroups.com
Some qt dev tips I can think of...

If you are adding a new feature to the app, try and think about what it might take if you are asked to add 5 more similar features, and whether you can position this first feature to be extensible. 
For instance, if someone wants you to make some functionality a user option and it is the first of its kind, it might be useful to think about developing that first option in a way that if you are asked to add 5 more options, it will be easy and maintainable to do so. Qt apps can become unwieldy if shortcuts are made or care isn't properly taken. It will always bite you later. Similarly, if you are trying to do a feature and while it may feel kind of hacky to do so, it is super convenient for you to just reach into the underlying implementation details of some other widget and access its private bits, there is a chance that will also bite you later. 
It might take a bit longer to implement that first feature, but will be super fast and easy when you get asked to do a bunch more like it. I always get really excited when I have to go add something and realize that it was already set up super nice (by myself or someone else) to extend. 

Learn how to write your UI layouts in pure code
Qt Designer is great, and I used the hell out of it in the past. But it ends up being so much simpler when you remove that component from your workflow and don't have to worry about updating a UI file or depending on an extra packaged resource (whether you either convert it with uic or load it at runtime). You can still keep the UI logic separate from the business logic in the same way, if you want, by writing a layout-only class and then either subclassing it or composing it. 

If you haven't tried using the Qt Resource system, check it out. It's awesome.
It is similar to the uic tool for converting a UI file into a python module, but instead it is for taking resources like images, text, or whatever you want and encoding it into a python module. This module can then be accessed directly by things like QPixmap/QIcon/QFile. Your application then no longer has to worry about where your resources will live in relation to your application and you don't have to go to disk to read them. All you have to worry about is the resource module being imported into your app. 

So if you have an image that you use like this:
pix = QtGui.QPixmap("/path/to/my/image.png")
You can instead put it into your .qrc file (which is XML), and (re)run your rcc command to generate the module:
 <!DOCTYPE RCC><RCC version="1.0">
 <qresource>
     <file alias="images/myimage.png">/path/to/image.png</file>
 </qresource>
 </RCC>
Then you do something like: $ pyside-rcc myresources.qrc -o myresources.py

And now you can refer to it in your application like this:
import myresources
...
pix = QtGui.QPixmap(":/images/myimage.png")
The "alias" thing is cool because you can give the path a completely different reference name in your app and it lets you swap out the real image with another, but the name your application uses does not change. And it lets you aggregate them into logical groups.

If you take the plunge and get into QAbstractItemModel, then try running your implementation through ModelTest to check for errors
Someone was nice enough to have written ModelTest first for Qt (C++) and then other people were nice enough to port it to PyQt/PySide. It just contains a bunch of validation tests against a model you have implemented, to see if it has any of the common errors people make when using this model system. It is pretty basic. You just create an instance of your model (or even a proxy that is set with your model) and then make an instance of the ModelTest. If it has errors, the constructor will fail right away and you can go inspect the test that failed, and fix your model until it passes:
from ModelTest import ModelTest
...
myModel = MyCustomModel()
myModel.populate()
test = ModelTest(myModel)
print "If you are reading this...I didn't fail hard"
Disclaimer: I did have to make one or two little tweaks to ModelTest because it seemed to care about some things that weren't totally necessary, but the output is pretty obvious to understand, when it points you to the exact test line that failed. 

This thing helped me completely solve a stupid bug I had in a tree model.

Be careful of widget leaks when creating temporary/transient objects
This is actually surprisingly easy to overlook (I did it recently actually) and is a subtlety of the parent/child relationships in Qt. You don't always have to set the parent for a widget, say, when putting it into a layout, since the Python reference counting will keep the object alive (or it might be set with the parent automatically by Qt). But sometimes you have to set the parent to keep a widget from being destroyed by garbage collection, or you want it to be relative to its parent for whatever reason. Fact of the matter being, you need to set a parent in some case. An example is a dialog popup or a menu. Lets say, on the fly, you want to pop up a QDialog so you do something like:
def show_a_dialog(self):
    # self our main window 
    d = MyDialog(self)
    ...
    d.show()
In this situation, a dialog gets shown that isn't modal or anything. But when you close it what happens? The instance of that dialog still stays alive because of the parent/child relationship, for as long as your parent main window stays around. Same happens with temporary menus and stuff that you pop up for a bit and then close. So the solution is to either use the close attribute or deleteLater():
# If your widget has a closeEvent
d.setAttribute(QtCore.Qt.WA_DeleteOnClose)
d.show()

# or just telling it to delete later if you are using exec_
d.exec_()
d.deleteLater()
There are other ways to clean it up I am sure. But all in all the point is to try and be aware of if and when your instances are not getting cleaned up when you think they are. I made a super quick and dirty test class to help me look at what is going on in my app and find leaking widgets:  https://gist.github.com/justinfx/11016801

Tidbits about Signal/Slots
Signals and slots can be confusing to debug when you have a lot of them and stuff is firing for reasons you can't understand. This is a silly-simple snippet that can dump the call stack so you can see what the hell is triggering that slot:
import inspect

def trace():
    frames = (str(f[1:4]) for f in inspect.getouterframes(inspect.currentframe())[1:])
    print "\n".join(frames)
    print '\n'
So somewhere in a given function you can put a call to trace(), and it will print the call stack for you to look at.

Also, it helps to think about signal connections and remember they aren't threads. When you connect 10 things to a given signal (unless you are doing it in a threaded environment), by default when that signal emits, it will iterate over all 10 of the connected slots and execute them in order. There are options to say a connection can be a queued connection, where the slot will be run in the event loop, if that suits your specific type of behavior. But otherwise, to keep in my how many things will run back to back before the event loop gets a chance to have another run. 

This also leads into mentioning another crazy quirk with signals/events + using QApplication.processEvents(). I recently saw a situation where Python hit the recursion limit because something that was part of a slot/event call stack used processEvents() which then created another recursive call, and continued to trigger again and again. Originally it was segfaulting, but after moving some code around, the seg fault was prevented and the recursive call was able to be seen as a python exception. The best small example I could find from a search was something like this 

Please don'ts...
(Just some random thoughts about stuff that would be best avoided. Figured it might be nice to extend this conversation with another section)
  • Don't do  from PyQt4.QtCore import * It imports the entire massive set of the QtCore/QtGui members in your module namespace and then your module namespace is bloated if anyone ends up inspecting it as a library module.
  • Don't put show() calls in the constructor of your widgets. Widgets should be able to be reused and a constructor is meant to initialize the widget. Not to show it or produce other unexpected side-effects.
  • Don't put show/exec calls in the root of your module. A dialog shouldn't be popping up just because you import something. It should be behind an if __name__ == "__main__": if it needs to have entry-point executable code.

That's all I can think of for now. If anything else comes to mind, I will tack it on.

-- justin

Fredrik Averpil

unread,
Jun 9, 2014, 4:31:46 PM6/9/14
to python_in...@googlegroups.com
Awesome write-ups, guys! :)
Reply all
Reply to author
Forward
0 new messages