Wednesday, October 19, 2011

Nexsys: NavigationWidget

While the Main Window is going to be the container for our filesystem browsing, the real heavy lifting is going to come from a separate custom widget - the NavigationWidget.

This tutorial will start us going through the creation of this smaller component that we'll then use inside of our main window.

By the end of this tutorial, your application should actually start navigating the folder system and look like this:





Creating the Widget Class

The first step we're going to do is save our nexsys/gui/nexsyswindow.py file as nexsys/gui/navigationwidget.py since we're going to reuse the loading logic and imports.

We're going to modify the code a bit, we're going to rename NexsysWindow to NavigationWidget, and switch the base class for the widget to QtGui.QWidget vs. QtGui.QMainWindow.

Ultimately, you should just end up with a simple shell widget class, similar to our original main window code, and update the docstrings to reflect the new class.

This is what the final file looks like for me:

insert code here

Creating the Widget Ui

We're going to fly through the user interface for this widget, because now that we've covered all the topics that you're going to need in the previous tutorials - you should be an expert!

This time, we're going to go back into Designer and create a new Widget by doing:  File > New > Widget > Create.

This widget will be considerably simpler than our main window.

Drag & drop in a new QLineEdit from the Widget Box > Input Widgets > Line Edit and place it in the top left of your widget.  Rename this widget to 'ui_path_edit'.

Next drag in 2 QToolButton's from the Widget Box > Buttons > Tool Button and place them one after the other, just to the right of the line edit.  Rename these buttons to 'ui_goup_btn' and 'ui_gohome_btn'.

Lay those 3 widgets out by clicking in the 'Lay Out Horizontally'.

Next, drag in a QTreeView (I'm switching it up on you this time) from the Widget Box > Item Views (Model-Based) > Tree View and place it just underneath the layout we just made.  Rename this widget to 'ui_contents_treev'.

Layout the whole widget using the 'Lay Out Vertically' option, and remove all the excess space for all the layouts.

Next, select both the 'ui_goup_btn' and 'ui_gohome_btn', and find the autoRaise property, and check it on.  Also, find the icon property for each, and set the icons to nexsys/resources/img/go_up.png and nexsys/resources/go_down.png respectively.

Finally, selectthe 'ui_contents_treev' again and set these properties and values:
  • property: rootIsDecorated value: checked off
  • property: itemsExpandable value: checked off
  • property: alternatingRowColors value: checked on
What you should end up is a widget that looks similar to this:


Go ahead and save this file as nexsys/gui/ui/navigationwidget.ui and we'll use this as our base ui configuration for the navigation widget.

Updating the NexsysWindow

Now that we have a basic configuration for our widget, we can import and use it in our main application to replace out the stub options in our tabs.

After the import nexsys.gui import, add a new line that reads: from nexsys.gui.navigationwidget import NavigationWidget

Alter your NexsysWindow.__init__ method to read:

    def __init__( self, parent = None ):
        super(NexsysWindow, self).__init__(parent)
        
        # load the ui
        nexsys.gui.loadUi( __file__, self )
        
        # clear out the current tabs
        self.ui_left_tab.clear()
        self.ui_right_tab.clear()
        
        # hide the headers
        self.ui_left_tab.tabBar().hide()
        self.ui_right_tab.tabBar().hide()
        
        # create the default tabs
        self.ui_left_tab.addTab(NavigationWidget(self), '')
        self.ui_right_tab.addTab(NavigationWidget(self), '')
        
        # create connections
        self.ui_newfile_act.triggered.connect( self.createNewFile )

This will clear out the current widgets that were loaded in from designer, hide the tab bar (we'll unhide it when we have multiple tabs), and then add a new navigation widget to each tab widget in our main window.

The full code for the nexsyswindow module should now read:

#!/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

from nexsys.gui.navigationwidget import NavigationWidget

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 )
        
        # clear out the current tabs
        self.ui_left_tab.clear()
        self.ui_right_tab.clear()
        
        # hide the headers
        self.ui_left_tab.tabBar().hide()
        self.ui_right_tab.tabBar().hide()
        
        # create the default tabs
        self.ui_left_tab.addTab(NavigationWidget(self), '')
        self.ui_right_tab.addTab(NavigationWidget(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' )

And, if you run your application now, you should see something that looks like this:



Updating the NavigationWidget

Finally, we'll want to actually hook up our widget to look at the folder system.

If you remember reading through the PyQt Coding Style Guidelines in the earlier tutorial, I recommend using QTreeWidget's over QTreeView's, and yet in the very next application - I am going back on that suggestion.

When dealing with systems that Qt has already developed model's for, the QTreeView is far superior.  Have a look at the QAbstractItemModel class to see all the inherited models that Qt provides - any functionality provided there should not be duplicated with an item-based widget.  Its mostly when you are developing your own new custom interfaces that the item-based widgets come in handy.

In this scenario, we're going to be dealing with accessing and navigating the filesystem, and for that the QFileSystemModel and QDirModel exist.  (Note: the QDirModel is actually deprecated - so depending on your version of Qt - don't use it)

What we're going to to now to our navigation widget is apply a completion system to the line edit, so that when a user types in their file path in the line edit, it will update to show them what paths are available.  We'll also apply a filesystem model to the tree view to display the contents of the filesystem at the set paths.

First, this is the whole code that we're going to be working with in the navigationwidget.py module:

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

""" Defines the NavigationWidget 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'

import os.path

from PyQt4 import QtCore, QtGui

import nexsys.gui

class NavigationWidget(QtGui.QWidget):
    """ 
    Creates a reusable file navigation widget to move around between 
    different folders and files. 
    """
    
    def __init__( self, parent = None, path = '' ):
        super(NavigationWidget, self).__init__(parent)
        
        # load the ui
        nexsys.gui.loadUi( __file__, self )
        
        # create the main filesystem model
        self._model = QtGui.QFileSystemModel(self)
        self._model.setRootPath('')
        
        # determine the version of Qt to know if the QFileSystemModel is 
        # available for use yet
        if ( int(QtCore.QT_VERSION_STR.split('.')[1]) < 7 ):
            completer = QtGui.QCompleter(QtGui.QDirModel(self), self)
        else:
            completer = QtGui.QCompleter(self._model, self)
        
        # set the completion and model information
        self.ui_path_edit.setCompleter(completer)
        self.ui_contents_treev.setModel(self._model)
        
        # assign the default path
        self.setCurrentPath(path)
        
        # create the connections
        self.ui_path_edit.returnPressed.connect( self.applyCurrentPath )
    
    def applyCurrentPath( self ):
        """
        Assigns the current path from the text edit as the current path \
        for the widget.
        """
        self.setCurrentPath( self.ui_path_edit.text() )
    
    def currentPath( self ):
        """
        Returns the current path that is assigned to this widget.
        
        :return     str
        """
        return str(self.ui_path_edit.text())
    
    def setCurrentPath( self, path ):
        """
        Sets the current path for this widget to the inputed path, updating \
        the tree and line edit to reflect the new path.
        
        :param      path | str
        """
        # update the line edit to the latest path
        self.ui_path_edit.setText( os.path.normpath(str(path)) )
        
        # shift the treeview to display contents from the new path root
        index = self._model.index(path)
        self.ui_contents_treev.setRootIndex(index)

Step-by-Step through the __init__ method


The first few lines of the init method you are familiar with by now, so we can skip them.

        # create the main filesystem model
        self._model = QtGui.QFileSystemModel(self)
        self._model.setRootPath('')

The first new line is the line where we create the QFileSystemModel instance.  As the docs state, this is the preferred model when working with the filesystem, and it won't actually start pooling the filesystem until a root path is specified.  Setting the root to a blank string allows us to access the full filesystem, though its already sectioned off to only look at parts as needed under the hood.

        # determine the version of Qt to know if the QFileSystemModel is 
        # available for use yet
        if ( int(QtCore.QT_VERSION_STR.split('.')[1]) < 7 ):
            completer = QtGui.QCompleter(QtGui.QDirModel(self), self)
        else:
            completer = QtGui.QCompleter(self._model, self)

This last bit creates the completer we're going to use on our QLineEdit.  This part is where it gets a little tricky, as if you're using Qt 4.7+, you'll want to stick with the QFileSystemModel instance - but previous versions, the completion mechanism doesn't seem to have been finished - so you'll have to use a QDirModel instead.

This code will check the QT_VERSION_STR variable to determine what version of Qt we're running to decide on the proper model to load.

The following lines just setup the basic configuration for our widgets by setting the completer, model, and currentPath.

        # set the completion and model information
        self.ui_path_edit.setCompleter(completer)
        self.ui_contents_treev.setModel(self._model)
        
        # assign the default path
        self.setCurrentPath(path)

After all parameters are setup - we create our connections.  Right now, we'll just listen for when the user hits enter on our line edit to apply their path changes.

        # create the connections
        self.ui_path_edit.returnPressed.connect( self.applyCurrentPath )

Step-by-Step through the currentPath methods

The currentPath methods are just accessor methods to the ui_path_edit text. We'll return the value from the text, and allow a developer to set the value - when we set the value, we'll also update our index for the widget as well.

The first two methods should be self-explanatory, and the only thing of note on the third one is how we're manipulating our tree widget:

        # shift the treeview to display contents from the new path root
        index = self._model.index(path)
        self.ui_contents_treev.setRootIndex(index)

The way to manipulate a QTreeView using a QFileSystemModel model is by setting its root index to the index of a particular folder. This will shift all of the contents of the tree up to that folder level.

So why don't we actually see a tree?

The settings that I had you change in the ui file have made our tree behave more like a list with columns, which is what we want out of the box for this widget. The rootIsDecorated property being set to off removes the expand/collapse icon from all items at the root level of the tree. The itemsExpandable property is blocking the ui from allowing the user to double click on an item to expand it.

Try taking those options off in the ui file, and see how your application reacts.

At this point - you should be able to navigate the folder system in a relatively simple way, and your application should look like this:

Pretty easy right?  Considering we still really haven't coded very much.  PyQt allows us to do a lot out of the box.



Coming up Next

We're going to keep going into more of the features that we've blocked out as we start tying together the actions that we've made from the main window to our newly created NavigationWidget.

No comments:

Post a Comment