Tuesday, October 18, 2011

Nexsys: Running the Application

The last few tutorials have been almost entirely based in the Qt Designer, and with our Main Window now mostly complete, it's time to jump back into the code.

While each of those tutorials was long and may have taken a long time to go through (or maybe not, I don't know), the more you get into this workflow and become comfortable with the Qt toolset - the faster you'll be able to get those ui mockups done.

The next step though is to start putting that UI to use.



Running the Application

With the code as it is, your folder structure should now look like:

nexsys/
 |- gui/
 |   |- ui/
 |   |   - nexsyswindow.ui
 |   |- __init__.py
 |    - nexsyswindow.py
 |- resources/
 |   |- img/
 |    - __init__.py
 |- __init__.py
 |- main.py
  - settings.py

Go ahead and re-run your application now with your UI complete to see it in all its glory through your app vs. designer's preview.

Pretty sweet right?

....or are you sitting there pulling your hair out, yelling at the computer because all your icons aren't showing up?  That happens to me too, I hate working through a tutorial only to end up with the results not being what I expect, worrying I have to go back through and debug my code.

You may still have to - don't get me wrong - however, you're also not at that desperate point yet, as I've anticipated this.

Improving the Loading Method

Remember in 1st grade when the teacher told you that numbers just couldn't be less than zero, so don't worry about it?  And then you get to 2nd grade, and all of a sudden "negative numbers" came up?  I felt like my teacher lied to me, I was very dissapointed.

Well, I'm going to do that to you now.

Up until now, we've been copying and pasting, or duplicating manually, our ui loading logic.  Don't do that...  Every good programmer is lazy, and should only want to do something once - I don't actually do that in my code.  I put my method into a library that I use for all of my classes since the code itself is generic and based on the relative path of the module and the classname.

There's also one more step missing that is needed to load the icon files properly, and that is that PyQt needs Qt's working directory to be the directory where the file is loaded from.  The icon's relative path of '../../resources/file_new.png' only works if Qt thinks the current path is the path of the uifile, which in most cases it won't be.

So, inside your nexsys/gui/__init__.py file, lets append the following code:

import os.path

import PyQt4.uic
from PyQt4 import QtCore

def loadUi( modpath, widget ):
    """
    Uses the PyQt4.uic.loadUI method to load the inputed ui file associated
    with the given module path and widget class information on the inputed
    widget.
    
    :param      modpath | str
    :param      widget  | QWidget
    """
    # generate the uifile path
    basepath = os.path.dirname(modpath)
    basename = widget.__class__.__name__.lower()
    uifile   = os.path.join(basepath, 'ui/%s.ui' % basename)
    uipath   = os.path.dirname(uifile)
    
    # swap the current path to use the ui file's path
    currdir = QtCore.QDir.currentPath()
    QtCore.QDir.setCurrent(uipath)
    
    # load the ui
    PyQt4.uic.loadUi(uifile, widget)
    
    # reset the current QDir path
    QtCore.QDir.setCurrent(currdir)

What we're doing here is taking the logic that we've been using to load our ui's up until now, and putting it into a common place - so all of the widgets in our application can use the same loading logic.

We're also going to change our main window code in the nexsys/gui/nexsyswindow.py file to read:

from PyQt4 import QtGui

import nexsys.gui

class NexsysWindow(QtGui.QMainWindow):
    """ Main Window class for the Nexsys filesystem application. """
    
    def __init__( self, parent = None ):
        super(NexsysWindow, self).__init__(parent)
        
        # load the ui
        nexsys.gui.loadUi( __file__, self )

Now, try running your application again.  This should now load all of your icons from your ui file properly into your window.  You now will also have a reusable loading logic to load the rest of the windows in our application - regardless of where they reside on the folder structure, since everything is running of relative paths.

(If you don't see any icons at this point - feel free to start pulling your hair out and screaming.)

Creating the First Connection

Now that we've done all of this work - lets create a connection to our first action.

For now - lets do something very simple to illustrate the different areas that an action can be run from.

At the bottom of your NexsysWindow class's __init__ method, append this code:

        # create connections
        self.ui_newfile_act.triggered.connect( self.createNewFile )

Also, add this method to your class:

    def createNewFile( self ):
        """
        Prompts the user to enter a new file name to create at the current
        path.
        """
        QtGui.QMessageBox.information( self, 
                                       'Create File', 
                                       'Create New Text File' )

If you re-run your application, if you choose Menubar > File > New Text File..., or click the New Text File... button on the toolbar, or click Ctrl+N, you should see a popup saying 'Create New Text File' for each type of trigger.

Now that is pretty cool, and if you think so too, welcome my fellow nerd.

Right now, this is all the code that you should have in your nexsys/gui/nexsyswindow.py module:

#!/usr/bin/python ~workspace/nexsys/gui/nexsyswindow.py

""" Defines the main NexsysWindow class. """

# define authorship information
__authors__     = ['Eric Hulser']
__author__      = ','.join(__authors__)
__credits__     = []
__copyright__   = 'Copyright (c) 2011'
__license__     = 'GPL'

# maintanence information
__maintainer__  = 'Eric Hulser'
__email__       = 'eric.hulser@gmail.com'

from PyQt4 import QtGui

import nexsys.gui

class NexsysWindow(QtGui.QMainWindow):
    """ Main Window class for the Nexsys filesystem application. """
    
    def __init__( self, parent = None ):
        super(NexsysWindow, self).__init__(parent)
        
        # load the ui
        nexsys.gui.loadUi( __file__, self )
        
        # create connections
        self.ui_newfile_act.triggered.connect( self.createNewFile )
    
    def createNewFile( self ):
        """
        Prompts the user to enter a new file name to create at the current
        path.
        """
        QtGui.QMessageBox.information( self, 
                                       'Create File', 
                                       'Create New Text File' )

Overall - we've been able to get a lot out of the Designer, considering how little code we're actually using to create our application.  I hope you guys agree, and are itching to get back into designer - because we've actually gotten as far as we can with the designer as it is.

We don't need to work on our Main Window much more at the moment, but right now it really is just a container shell - we'll next need to focus on creating the navigation widget.

Coming up Next

The bulk of the code for actually navigating the filesystem is going to come from a separate widget, not the window itself.  The window is just the casing that facilitates the communication between the navigation widgets.

So, next up - we gotta create that one.

1 comment:

  1. The following should be a big help to anyone following along with PySide.

    PySide's ui loader differs from PyQt's in that rather than populating a given widget from the information in the ui, it returns a new widget from the information in the file instead. This requires a bit of modification in a couple of places.

    1) The gui/__init__.py above. Here is how I have it working with PySide (with a little extra modification to get PySide's loader to find icons by relative paths)
    import os.path

    from PySide import QtUiTools, QtCore

    def loadUi(modPath, widget):
    """
    Loads a widget from a .ui file
    """
    basename = widget.__class__.__name__.lower()
    uiFileDirPath = os.path.join(os.path.dirname(__file__), 'ui')
    uiFilename = os.path.join(uiFileDirPath, '{0}.ui'.format(basename))

    loader = QtUiTools.QUiLoader()
    uiFileDirPath = QtCore.QDir(uiFileDirPath)
    # setting working dir for the loader so it can follow the relative paths to
    # resources from the .ui file
    loader.setWorkingDirectory(uiFileDirPath)
    widget = loader.load(uiFilename)
    return widget


    2) The MainWindow class will also then be slightly different in a way that should affect the naming conventions mentioned in an earlier post. Here are the relevant bits:
    class MainWindow(QtGui.QMainWindow):
    """ Main window class """
    def __init__(self, parent=None):
    super(MainWindow, self).__init__(parent)
    self.ui = loadUi(__file__, self)

    #to interact with a widget places by designer, it will look like the following:
    ui.statusBar.showMessage('Hello', 2500)



    Someone let me know if I've done it in a silly way, but this is how got it working.

    ReplyDelete