Wednesday, October 12, 2011

Nexsys: Starting a Full Application

So far, the tutorials we've been doing have all been single scripts, and isolated from each other.

Now that we have a basic understanding of what Qt is, we're going to go more in depth on the real power of what Qt can do.  We'll do this by starting to build a full application.

This is going to be the first of many tutorials designed to take you step-by-step through my thought process as I develop a new application using Qt.



The first thing to do, is figure out what you're going to build.

Since I cannot share actual production code that I've worked on, I have to pick an arbitrary project for us to do together.  I want to make something that will touch upon a variety of features of Python/Qt, that will hopefully be relatively interesting to work on, relatively simple to work on, and contain as few additional dependencies as possible.

So lets make a simple Total Commander style file browser.  Why not, I love Total Commander and wish I could have the same application in Linux and Mac (vs. the slightly different version equivalent for each), so lets make it.

Don't worry, with Python and Qt - its really not as complex as you'd think.  It won't require any custom drawn widgets, and will utilize many of the built-in features of the cross-platform language and framework methods that Python/Qt provide.

Getting Started

The very first thing that I do when starting a new program is asses its overall complexity.  Simple 1 off scripts for something like a 3rd party plugin can be a simple module.  Anything more complex than that should be created as a Python package instead.  If this is a new concept, don't worry - its very simple.

A python module is just a python file that can be imported or executed as a self-contained entity.  Every script we've written so far are modules - just individual files. 

A python package is a group of files together in a folder, containing a specific file __init__.py that registers the folder as a namespace.

For a much more detailed understanding than I'm going to give you - I'd recommend reading through python's module documentation.

This is generally how I always create a new application (replacing project with my actual project name):

project/
  |- api/
  |     - __init__.py
  |- gui/
  |    |- ui/
  |    |    - projectwindow.ui
  |    |- __init__.py
  |     - projectwindow.py
  |- resources/
  |     - __init__.py
  |- __init__.py
  |- main.py
   - settings.py

Sometimes, I don't need all of the packages and sub-packages - and when I don't, I simply delete them.  However, I can't tell you how many time's I've thought I didn't need something like an api layer at the start of a project and had to add it in later.  I've determined at this point, its better to assume there's always the potential for an api or gui layer to be added, so its best to assume that I should be able to add it in later.  

Naming the Project

Naming the project is surprisingly important and difficult.  Ideally, you'd end up with a clever, witty name that both describes your project and stands out as an individual name.

Unfortunately, this is ever increasingly difficult as there are so many applications and systems out there.

Feel free to name your project whatever you wish, I'm not putting too much thought into this part - so I'm naming mine nexsys (next system, nexus... I dunno...it sounds cool and kinda fits).  If you name your application something different, just remember to replace all the references to nexsys with your own application's name.  

Creating the Project

Now that we have figured out what we're going to call our project - lets create our folders and files according to the above template. For right now, we'll only create the root level, so you should end up with:

nexsys
  |- __init__.py
  |- main.py
   - settings.py

And if you're following along with the filepathing, this would be a folder in ~/workspace/pyqt or c:/workspace/pyqt.

As we add each component and sub-package to the appliation, I'll explain what each level is for:
  1.  __init__.py: required python module to designate nexsys as a package.  We'll put our global project information into this folder such as the version, author, credits, and licenses.
  2. main.py: borrowing the paradigm from C++, all my applications contain a main.py (or main.pyw file for windows to run without a shell background) and will be where the QApplication instance is created and run from.  This is the main entry point for our application.
  3. settings.py: defines a module that will contain global settings for the whole project that each layer can access.  This can contain any number of values - server settings, global variables, etc.  For now, this will be blank until we need it.
So, this is the root python package for our application - when put into a location that python can import from (like $PYTHONPATH/lib/site-packages).

__init__.py

So, you may have just made some blank files, or not actually made the files.  Either way, we're going to replace them now...

While there isn't a definitive answer on the best/proper format for a python header file, there are a couple of useful suggestions on this post, information on docstrings here, and encoding options in this pep.

We're going to create our __init__.py file to contain the information about our package.

#!/usr/bin/python ~workspace/nexsys/__init__.py

""" 
nexsys is a cross-platform file browser system designed using PyQt.

It provides a lightweight, simple and consistent, split-pane navigation 
system that will run on all major desktop operating systems.

The base project was developed as a tutorial for the 
[http://bitesofcode.blogspot.com Bites Of Code] blog.
"""

# 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'

# define version information
__requires__        = ['PyQt4']
__version_info__    = (0, 0, 0)
__version__         = 'v%i.%02i.%02i' % __version_info__
__revision__        = __version__

For the sake of my examples, I'm including the path to the file in the first line of code - you don't really need to do that...

Generally speaking - these are the variables that I would set at the root for every application.

The first line lets the system know (theoretically) what application to use to run the file.

The very next section is the docstring which will be used to provide documentation about the module.  Python has an internal documentation processing system and it is highly recommended that you follow their structure to take advantage of it.  Individual documentation syntax's can be driven by your docgen of choice as there are a number of options (like Miki).

The other variables provide some meta data to your package.  Some applications use one or another variable (like __author__ vs. __authors__) so I find it best to double up.

Generally though, these are the variables I set:
  1. __authors__: list of all programmers who worked on this project.  Something like docgen will amalgalate all sub-package/module authors when generating its author and credit list, other systems you should include all authors in your root file.
  2. __author__: comma separated list of authors, can be just auto built from the __authors__ list
  3. __credits__: list of additional contributors - depending again on what is using this data, you can populate it what you want.  I usually have a tuple of strings containing (1) the contributer, and (2) what they contributed.  For instance, if you get icons for your program, you should include the creator of the icon in your credits.
  4. __copyright__: copyright information based on your company's requirements
  5. __license__: the license this code is released under - this will vary based on your project and company requirements.
  6. __maintainer__: primary developer for the code as it currently stands - this should be the person who is contacted for changes, and unlike the authors variable, should change as developers change
  7. __email__: email of the primary developer for the code
  8. __requires__: list of required dependencies for this project so it can be in a visible location
  9. __version_info__: tuple containing (1) major version, (2) minor version, (3) fix version
  10. __version__: string representation of the __version_info__ variable
  11. __revision__: some systems require this, defaults to setting it to the same as the __version__, however, a better use is to auto-set this value if you are using something like SVN
Variables 1-7 should be included in all modules you write (including the first line and docstring).  I only include variables 8-11 in my base package's __init__.py file.

(Obviously, you should change these variables to reflect your own name, email and information where appropriate.)

settings.py

For now, our settings file will be empty - so all we need is the standard template header (which, by the way, if your editor supports macros you should save as a macro that you can reuse.  And if your editor doesn't support macros - you should get one that does...)

So, you can just delete variables 8-11 and save as settings.py in your package folder, and switch the docstring to something useful like """ Module to contain all global settings for the nexsys application. """

You should end up with something similar to:

insert settings code

main.py

Lets go ahead and save that settings.py file as main.py since we'll be reusing our header again.

We're also going to modify the file to read:

#!/usr/bin/python ~workspace/nexsys/main.py

""" Main entry poin to the nexsys application. """

# 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

def main(argv = None):
    """
    Creates the main window for the nexsys application and begins the \
    QApplication if necessary.
    
    :param      argv | [, ..] || None
    
    :return      error code
    """
    app = None
    
    # create the application if necessary
    if ( not QtGui.QApplication.instance() ):
        app = QtGui.QApplication(argv)
        app.setStyle('plastique')
    
    # create the main window
    QtGui.QMessageBox.information(None, 'Stub', 'Create the Main Window!')
    
    # run the application if necessary
    if ( app ):
        return app.exec_()
    
    # no errors since we're not running our own event loop
    return 0

if ( __name__ == '__main__' ):
    import sys
    sys.exit(main(sys.argv))

What we've done here is basically stub out our program.  If you did the Hello, World tutorial, this code should look pretty similar to you - its almost exactly the same as the end result of that example.

The main difference between this code and that, is that we've split out our main application logic into a function called main instead of calling it directly from the if ( __name__ == '__main__' ): logic section.

This way, we're opening up the possibility for us to access it as a function from importing the package vs. requiring to only be run as a script from the main level.

If you do:

>>> import nexsys.main
>>> nexsys.main.main()

This would be the same as running the file directly. Had we put that logic in the logical condition, it wouldn't be accessible from an import.

We're also doing app.setStyle('plastique')  This is not required by any means.  This is more of a personal preference of mine.  When you develop cross-platform Qt applications, the style your program will be in will default to the OS style - which can be good, or can be bad.  Forcing it to a particular style ensures that your window will look the same regardless of which OS its running in...and I personally like the plastique style best.  Feel free to not use that - or use your own style!

The final difference is the usage of the sys, sys.argv, and sys.exit module and commands.  This is just passing data to and from our application from command line.  If we want to support command line interface to our application, we simply need to insert a layer to process the sys.argv parameters.  For right now, we're not really needing them.

Coming Up

So, we've got our hello world application running in a package now.  So what?  Next up - we're going to start blocking in our main application window, subbing out that stub MessageBox for something more useful.

2 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. When this is run, it runs, but comes up with an error:
    QApplication: Invalid Display* argument

    What does this mean?
    Also the terminal window won't close either

    ReplyDelete