Tag: PyQt5

Comparing Python Tkinter and PyQt5

Comparing Python Tkinter and PyQt5

TkInter is the well-known GUi system based on the Tk toolkit and interfaced to Python so that it has become the preferential GUI interface system. PyQt5 is an analogous user interface developed and support by Riverbank Computing, and is essentially a set of bindings for the QT platform developed by the Qt company. PyQT5 is available under a GNU GPL v3 license, or as a commercial license. This essentially means that you can use PyQt5 freely, but you must distribute your source code with any product you build using PyQt5. Or, you can buy a commercial license.

GUI using Tkinter

GUI using PyQt5

The two figures above show the same interface developed in Tkinter and PyQt5. They show that you can build pretty much the same kind of GUI with either system.  I spent a week learning PyQt5 and building the new interface to match the one I had already built in Tkinter. Each system has different advantages and disadvantages, and we’ll summarize them in the article that follows.

The tkinter version is fully integrated with the database, but I only bother to connect the right hand searchbox and listbox to the database in PyQt5. The rest are just hardcoded,

But basically, the systems do about the same things and are about equally easy (or hard) to use, and you can’t go wrong using either of them. There was no clear winner of this experiment: once you have climbed the learning curve you can build nice-looking systems either way.

I built the Tkinter version of this interface as part of a larger development project to interface our opera company’s data to a new MySQL database, where building a UI gave us the ability to view the information in more flexible ways. This screen represents a way to create a cast list for the current and previous shows and generate spreadsheets of those casts for the Board , the cast and the directors to work with. The left-hand table is the final cast, sorted by role-type and sex, which amounts to one simple database query.

It allows you to select any person in our database and assign them a role in the current or an older production.

I learned how to use PyQt5 from the on-line PyQT5 reference guide as well as from this short tutorial by Michael Herrman. He has also written a book on PyQt and there is a link to it at the bottom of the tutorial.  The Zetcode tutorial was also helpful. There is also a video tutorial at learnpyqt.com.

If you want to build a GUI using listboxes, buttons and checkboxes, you won’t have any trouble with PyQt. In fact, since the listbox automatically includes a slider, you will find it a bit easier.  It is also worth noting that all PyQt widgets have Tooltips: helpful phrases that can explain what a widget is for.

I already noted that the QRadioButtons work better if you derive a class that holds the button index or title, using a Mediator pattern.

Layout managers

Tkinter has two major layout managers, pack() and grid(). Pack arranges the objects in the frame you provide. Grid allows you set up an n x n  grid and place the objects in one or more grid cells.

PyQt5 has three layout managers: QHBoxLayout, QVBoxLayout and QGridLayout. You can add widgets to the box layouts and they will line up horizontally or vertically. The QGridLayout is similar to, but not the same as, the tkinter grid. The biggest single difference is that if you place a widget in a grid cell, it expands to fill the entire cell. In order to put a button in a grid and not have it stretched to fill the cell, you have to add a QHBoxLayout inside the cell and then perform a hbox.addStretch(1) before and after the button to center it. These are essentially spacers that grow to fill the space on either side of the widget.

QtDesigner

PyQt5 provides the QtDesigner app which allows you create layouts visually. It actually includes layouts, and you can at least look at what code it generates. However, the resulting file is of type .ui and you have to run the pyuic5.exe program to convert the .ui file to a Python file. Once you have done this and edited the Python file, you can never go back to the designer.

Events in Qt5 are referred to as signals and slots, where the event is a signal and the callback function is a slot.  You can easily write analogous programs in PyQt5 to handle events much as you do in tkinter. You can also do this in the QtDesigner, but the event interface in the designer is a lot of trouble to use. Writing the code yourself is easier, and you probably would want to modify the code anyway. And, again, once you change the code, you can’t do back to the designer.

PyQt5 Style Sheets

While PyQt5 widgets have a plethora of useful methods, the designers left out such things as changing colors and borders. For this you have to resort to CSS (Cascading Style Sheets).  You can find a whole web page of Qt5 CSS examples here.

For example, suppose you want a label to be blue instead of black. In tkinter, you would write:

lb1 = Label(cFrame, text="Character", foreground='blue')

But in PyQt5, you would have to write.

lb1 = QLabel("Character")
lb1.setStyleSheet("QLabel { color : blue; }")

This sets the style for all instances of QLabel, but you can specify an individual label as well.

A more difficult case was setting the border of the GroupBox (which is the same as a tkinter LabelFrame). I also changed the color of the text label here too.

self.app.setStyleSheet("""QGroupBox {border:1px solid black;
    margin-top: 10ex;  padding: 15 3 px; }""")
self.setStyleSheet("QGroupBox::title {"
                     "color: blue;"
                    "background-color: transparent;"
                    "padding-top: -50px;"
                    "padding-left: 8px;} ");

Setting these style sheets can be tricky because you do not get any errors if you leave something out, like that terminal semicolon.

Listboxes and Model-View-Controller

PyQt5 has a list box, a table widget and a separate Treeview widget. While each of them can be used as stand-alone widgets, they also can be used in the Model-View-Controller system, where the data is the Model, the widget is the View and the Controller is the user or some external event. Essentially this is useful when the data changes frequently and this will cause your table to be refreshed automatically. I haven’t tried this out yet, as it takes quite a bit of programming. However, there are list and table widgets you can use without getting into using MVC.

I found the table display troublesome, because while you could remove the gridlines and left hand column numbers, the lines were quite widely separated.  After consulting the informants on Stackoverflow, I found that you could code the line spacing a line at a time. It turns out that the height of each row is calculated from the top of the table, so for each row, you have to calculate it as follows:

self.castTable.setRowHeight(row, (row + 2) * 7)

It turned out, however, that the spacing in the Treeview looks a lot better, and I am switching to that.

Conclusions

Tkinter is better documented and may a bit easier to work with. But there are more widgets in the PyQt5 library, including date editors, a progress bar, a slider, a font picker, a toolbar, and a multiline text entry field. It also supports the MVC pattern and includes Tooltips, which might be helpful for new users. While Herrman felt there was a difference in the clarity of the widgets, I didn’t notice it.

PyQt Radio Buttons and the Mediator Pattern

PyQt Radio Buttons and the Mediator Pattern

In our previous article, we showed how to derive a new class from QRadioButton that keeps the index value of that button and keeps the click event callback right in the class itself.

In this simple article we are going to look at an easy program that fills an entry field with a text string based on which radio button you select. We also introduce you to the Mediator Design Pattern to help communication between widgets.

We came up with this little trick while writing a program to create a cast list for our operetta company. Leads play named roles, but for chorus member we just store their voice part. The radio buttons are labeled S, A, T and B, and when you click on one of them, the entry field is filled with the full name of that voice part: Soprano, Alto, Tenor and Bass. Since the entry field is editable, you can add more text like “2nd Tenor” where this might be useful, and that name is stored as part of the last list. 

Using a Mediator

When you click on any of the RadioButtons, the long name is copied into the entry field. The question is, how does it get there?  In effect, each of the four button instances need to communicate with the entry field. And, while that is not difficult to achieve, it doesn’t scale very well as your program grows to include more visual widgets. To simplify this problem, you use the Mediator Design Pattern and the Mediator class.

Rather than the various controls all sending information to each other, the Mediator class becomes the traffic cop that receives the various button clicks and other widget actions. Then, it knows about the other controls you might want to communicate with and passes the click information on to them. So, when we create instances of the derived vcRadioButton, you pass it the label it displays, the long name it is to send on to the entry field and a reference to the Mediator class:

You first create the Mediator and give it a reference to the entry field  (which is called QLineEdit in PyQt5):

# create the entry field
self.entry = QLineEdit()
grid.addWidget(self.entry,0,0)

# Create Mediator and
# pass it the entry reference
self.med = Mediator()
self.med.setEntry(self.entry)
# Create a GroupBox for the four radio buttons
self.voiceBox = QGroupBox("Voice part")
vcGrid = QGridLayout()

Then you create the buttons and pass each of them a reference to the Mediator

# create the four radio buttons and labels
vs = VcRadioButton("S", "Soprano", self.med)
va = VcRadioButton("A", "Alto", self.med)
vt = VcRadioButton("T", "Tenor", self.med)
vb = VcRadioButton("B", "Bass", self.med)


# and add them to the 2 x 2 grid
vcGrid.addWidget(vs, 0, 0)
vcGrid.addWidget(va, 0, 1)
vcGrid.addWidget(vt, 1, 0)
vcGrid.addWidget(vb, 1, 1)

Then we add the vcGrid to the voiceBox layout, and the voiceBox to the outer grid.

self.voiceBox.setLayout(vcGrid)
grid.addWidget(self.voiceBox, 1, 0)
self.setLayout(grid)

The VcRadioButton class

Our actual VcRadioButton class is even simpler than the one we wrote for the six radio button example, because we don’t have to store anything in a class variable: we just tell the Mediator that the button has been clicked.

class VcRadioButton(QRadioButton):
    def __init__(self, label, title, med):
        super().__init__(label)
        self.title = title  #save the title
        self.med = med      # and copy the Mediator reference
        # connect the button clicks to the comd method                                                   
        self.toggled.connect(self.comd)

    # returns title stored in this class instance
    def getTitle(self):
        return self.title
    # gets the title and puts it in the character entry field
    # using the Mediator
    def comd(self):
        radio = self.sender()   #get the button you clicked
        if radio.isChecked():   # if checked copy the title
            self.med.setVoice(radio.getTitle())

Note that the VcRadioButton connects the click event (called toggled in Qt5) to the comd method right there in the same class. And that comd method tells the Mediator to set the voice part into the entry field.

Then finally our Mediator is simple, since we are only mediating connections between radio buttons and the entry field. When the radio button is clicked, it calls the setVoice method in the Mediator, which copies the text into the entry field.

# The Mediator saves a reference to the entry field
# and copies text into the field when setVoice is called

class Mediator():
    # save the entry field reference
    def setEntry(self, entry):
        self.entry = entry
    # copy the text into the entry field
    def setVoice(self, text):
        self.entry.clear()
        self.entry.insert(text)
 

This may seem like a lot of running around to copy text into an entry field, but the Mediator quickly becomes quite important when your program needs to handle interactions among a number of widgets, such as list boxes, push buttons and check boxes. It is probably the most useful and significant of the 23 Design Patterns when you are writing user interfaces.