Saturday, October 8, 2011

Introduction to Designer

The Qt Designer is a WYSIWYG (what you see is what you get) interface for laying out the widgets for your applications.

It allows you to drag and drop widgets onto your custom class and lay them out with minimal effort and 0 code. Your template will then be saved out to an XML based file (a .ui file) format that you can read into your application.

In this tutorial, we'll re-do our last code using Qt's designer to create our actions and link everything together.




When used well, this is an invaluable resource - I personally would never create a widget that required static components without it. Working in this way allows yet another level of separation between View and Controller logic that will allow you to update and organize your interfaces without affecting your code.

It is also extremely easy to customize and extend using Python plugins and widget promotion, some concepts we'll get into in a more advanced course.
To really get to know designer well, you should read through the Qt documentation for it, as this will only be an overview on a simple way to use it.

Getting Started in Designer

Depending on where your Qt was installed to (and which version you're working with), the Designer will be located in different places, however it should be a designer(.exe) file in your QTDIR/bin/ folder.

Run that, and you should see a program that looks like this:






If you don't see this application - stop and go back.  Check out your Qt install, and make sure you're running the right application (don't run creator).  Otherwise, you will be very confused moving forward.

I'm not going to go too much into the basics - thats what the Qt docs are for.  Generally though, you can create your various widgets, child widgets, and layouts by dragging & dropping from the Widget Box area on the left onto any widgets in the main editor area.  You can also edit all the properties on your widgets, and setup your connections using the designer.

For now, we'll just layout and design our widgets - we'll save the more advanced options for a later tutorial.

Create the Main Window

Lets start creating the interface for our last example by creating a new QMainWindow window.  This is done by doing Menubar > File > New  and choose the Main Window option from the templates/forms section on the left.  Hit Create and you should now see a new window in your editor.

If you look in the Object Inspector in the right hand side of the Designer, you'll see that the template created our QMainWindow class, as well as the menubar QMenuBar object, the centralwidget QWidget object, and the statusbar QStatusBar object.

This will correlate with the objects we accessed via code before.  The icon with the QWidget object is showing the layout that is associated with the QWidget, and that it has not been assigned.

Create the Test Menu

The first thing we did in code before was create our test menu, and the actions that went with it.  So we'll do the same thing here.

In the top-left of your window, you should see a Type Here text box - this will allow you to type in new menu's and action's into the menubar via the designer.

Double-click on the area and type Test.  This just created a new menu called 'menuTest' in your window's menubar.  If you look in your Object Inspector again, this will now show a QMenu child for your menubar.

Lets go ahead and rename this object right now - we don't want to use 'testMenu' - so double-click on the object in the inspector and you should now be able to edit its name.  Reset the name to 'ui_test_menu'.  (you could also edit the 'objectName' property in the Property Editor as well)  In the next tutorial, we'll go over naming conventions and best-practices for why we just renamed this object.  You'll just have to trust me that there's a reason for it for now.

Create the Actions

So we now have our menu - lets create our actions.

If you click on the Test menu now, it'll popup a new area for you to type again.   This is where you'll be able to add new Action's to your menu.

To make sure we're on the same page - this is what you should be seeing at this point:


Double click on the Type Here for the popup menu 3 tiems to create our 'Exec Dialog', 'Show Dialog', and 'Show Count' actions.

Once you've finished doing that, you should now see 3 QAction classed children for our 'ui_test_menu' object in the Object Inspector.  Rename these actions to 'ui_exec_act', 'ui_show_act', and 'ui_count_act' respectively.

So thats all we had to do to add our actions.

Create the Tool Buttons

Now, we'll go ahead and create the tool buttons and lay them out in our central widget as we did before in code.

To do this, find the Tool Button in the list of widgets in the Widget Box in the left panel of the Designer, and then drag & drop them into your window.

You can just do this 3 times and in no particular layout for now.

You should see a window that looks like this at this point:


Underneath your 'centralwidget' object in the inspector, you'll now see 3 QToolButton classes.  Lets rename them 'ui_exec_btn', 'ui_show_btn', 'ui_count_btn' respectively.

Create the Layout

As I had mentioned before - there are 2 kinds of layout processes: layout out objects in a layout, and assigning a layout to a container.

In Designer - the to different ways are represented by selection counts.

To assign multiple widgets in a layout, you would select them each together and then click on one of the layout options in the toolbar.  This would lay each selected widget out into a particular configuration.

To assign a layout to a container, you would select just the root widget and then choose one of the layouts from the same toolbar.  This would take the child widgets for your widget and assign them to a layout on the widget (same as calling self.setLayout(vlayout)).

This is what the layout toolbar looks like:


It is found at the top of the designer in the toolbar area, and from left to right the buttons are:

  1. Lay Out Horizontally (QHBoxLayout)
  2. Lay Out Vertically (QVBoxLayout)
  3. Lay Out Horizontally in Splitter (QSplitter)
  4. Lay Out Vertically in Splitter (QSplitter)
  5. Lay Out in a Grid (QGridLayout)
  6. Lay Out in a Form (QFormLayout)
  7. Break Layout (removes the layout from the current selection)
  8. Adjust Size (resizes the selected widget to its ideal size based on its layout)
So, we'll do the next step that we had done through code previously and that is layout our buttons in a horizontal layout.

Select all three buttons and hit the 'Lay Out Horizontally' button from the toolbar.

This will now show a red border around your buttons - snapping them into place.

Another nice feature of the Designer is the ability to preview your layout and make sure things are sizing and acting the way you want.  To preview your widget, go to Menubar > Form > Preview or hit Ctrl+R.  This will now run your window so you can resize and see how it goes.

If you do this at this point, you'll see that there is no interaction with our buttons.  This is because we've not set a layout to our container widget yet - so Qt has no knowledge of how we want it to interact together.

Close down the preview, and select the window itself.  Now that we have the window selected, we're able to assign a layout to the window itself by clicking on one of the layout buttons.

If you remember from our code, we had created a QHBoxLayout and then nested that inside of a QVBoxLayout, so we'll do the same thing here.  With the root window selected, click on the 'Lay Out Vertically' button in the layout toolbar.

If you now run the preview you can see that we get a similar interaction with our buttons that we had before we added our stretches via code.

Adding Spacers

The way to add stretch to your layouts via the designer is to add Spacer objects to the designer widget.  This is done by dragging and dropping a Horizontal Spacer or Vertical Spacer from the Widget Box onto your form.

To recreate our bottom-right aligned button, we'll want to add a Vertical Spacer above our red-bordered horizontal layout, and then a Horizontal Spacer before our first button inside our red-bordered horizontal layout.

When you drag & drop into a layout, you'll see a blue line representation of where your drop will fall - so go ahead and drag your vertical spacer to the top of your widget - and let it drop when the blue line falls above the red border.  This should immediately snap the buttons to the bottom of the window.

Next, drag a horizontal spacer onto your form until you see a blue line within the red-bordered layout, to the left of the first button.

This will now snap your buttons to the right hand side.

Your form should now look like this:


If you hit Ctrl+R again to preview it - the window will now stretch the buttons to the bottom-right of your window.

For fun, try to get them to align to the top-right like our first example.

Loading into Code

Now that we have recreated our dialog the way we had programmed it, we will need to update our code to reflect the changes.

First thing we'll need to do is save our file out.  This should be saved to a sub-folder called 'ui' from where your code is, saving the file as 'mainwindow.ui'.  (For now, this is so you don't have to tweak the code from my example to your folder, but we'll go into folder structure suggestions in another tutorial).

If you're following the file structure I've been using, then you should've saved this file to ~/workspace/pyqt/layouts/ui/mainwindow.ui or for windows, c:/workspace/pyqt/layouts/ui/mainwindow.ui.

Lets now go back to our main.py file and modify the code to read:

#!/usr/bin/python ~/workspace/pyqt/layouts/main.py

import os.path

import PyQt4.uic
from PyQt4 import QtCore, QtGui

class SampleDialog(QtGui.QDialog):
    def __init__(self, parent):
        super(SampleDialog, self).__init__(parent)
        self.setWindowTitle('Testing')
        self.resize(200, 100)

class MainWindow(QtGui.QMainWindow):
    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)
        
        # load the ui
        relpath = 'ui/%s.ui' % self.__class__.__name__.lower()
        uifile = os.path.join(os.path.dirname(__file__), relpath)
        PyQt4.uic.loadUi(uifile, self)
        
        # assign the actions
        self.ui_exec_btn.setDefaultAction(self.ui_exec_act)
        self.ui_show_btn.setDefaultAction(self.ui_show_act)
        self.ui_count_btn.setDefaultAction(self.ui_count_act)
        
        # create the connections
        self.ui_exec_act.triggered.connect( self.execDialog )
        self.ui_show_act.triggered.connect( self.showDialog )
        self.ui_count_act.triggered.connect( self.showCount )
    
    def execDialog(self):
        dlg = SampleDialog(self)
        dlg.exec_()
        
    def showDialog(self):
        dlg = SampleDialog(self)
        dlg.setAttribute( QtCore.Qt.WA_DeleteOnClose )
        dlg.show()
    
    def showCount(self):
        count = len(self.findChildren(QtGui.QDialog))
        QtGui.QMessageBox.information(self, 'Dialog Count', str(count))

if ( __name__ == '__main__' ):
    app = None
    if ( not app ):
        app = QtGui.QApplication([])
    
    window = MainWindow()
    window.show()
    
    if ( app ):
        app.exec_()

So, what we have changed here, is we've removed all creation of widgets and layouts - this is now handled by the ui file loading.  We've also switched the references from 'show_btn' to 'self.ui_show_btn', as well as the other buttons and actions.  This will now match what is coming in from the ui form.

Loading the UI File

First, you will notice that we've added a couple more imports to the top of our file:


import os.path

import PyQt4.uic

We're going to use the os.path module to build our ui file location, and the PyQt4.uic module to load our ui form for our dialog.

The replaced loading logic for our form has become:

        # load the ui
        relpath = 'ui/%s.ui' % self.__class__.__name__.lower()
        uifile = os.path.join(os.path.dirname(__file__), relpath)
        PyQt4.uic.loadUi(uifile, self)

This is a little more complex than it really needs to be - but I will go over why we did it this way in the next tutorial.  For now, it just needs to work.

The next set of changes we made were to the connections:

        # assign the actions
        self.ui_exec_btn.setDefaultAction(self.ui_exec_act)
        self.ui_show_btn.setDefaultAction(self.ui_show_act)
        self.ui_count_btn.setDefaultAction(self.ui_count_act)
        
        # create the connections
        self.ui_exec_act.triggered.connect( self.execDialog )
        self.ui_show_act.triggered.connect( self.showDialog )
        self.ui_count_act.triggered.connect( self.showCount )

The only real difference here to the code from before is that we've named our objects differently, and they're now properties on our window vs. floating in the constructor.

Naming Conventions, Folder Structures and Best Practices

Some of the things we've done in this tutorial may seem like additional steps.  Which, in all fairness, they are.  But like with every tool - the Designer if used well can be an invaluable resource, but if used poorly can be a huge crutch.

One good way of making sure its used to its potential is to adhere to some common coding standards and naming conventions, so we'll go in-depth on that in a future tutorial.

1 comment: