Monday, October 17, 2011

Nexsys: Main Window Ui (Part IV)

In this section we're going to finish up our main window by adding some icons to our actions, and then look into how to reuse them for a toolbar.



Resources Module

When I start adding non-code resources to my projects, I first create a resources sub-pacakge to my application, so lets do that now.

Grab your nexsys/settings.py file and save it to nexsys/resources/__init__.py (creating the resources folder in the process).  You can change the docstring to something more accurate, like """ Provides methods to access various resource files for the application. """

Lets also add this code underneath all the variable declarations:

import os.path

BASE_PATH = os.path.dirname(__file__)

def find( relpath ):
    """
    Looks up the resource file based on the relative path to this module.
    
    :param      relpath | str
    
    :return     str
    """
    return os.path.join(BASE_PATH, relpath)

This method will let us import the resources module and lookup our files using their relative path to the module.

Your full file should look something like this:

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

""" Module to contain all global settings for 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'

import os.path

BASE_PATH = os.path.dirname(__file__)

def find( relpath ):
    """
    Looks up the resource file based on the relative path to this module.
    
    :param      relpath | str
    
    :return     str
    """
    return os.path.join(BASE_PATH, relpath)

Qt Resource System

The Qt Resource system involves the usage of qrc files that (in C++) get compiled as runtime resources that an application can use.  The Python support of this really isn't that great, there aren't dynamic loaders built-in that work the way the uic system works.

So, to get things like icons working in my designer files, I've had to hack around a little bit.  The easiest way I've found is to actually bypass the resource system entirely and handle the path loading for icons directly.

Icons

We'll get to the code on this in a minute, but first a word on icons in general.

Depending on the needs of your tool, you may need to create your own custom icons, and while I proudly tried to do this early on for all my icons, I found I ended up spending often as much time if not more on my icons than on the tool itself.  Considering all the applications I was building were for in-house use, finding open-content icons was a better use of my time.  If you're building commercial software, you'll need to check the licensing agreements before using such things.

I now almost exclusively use open-content icons that I can find on sites like www.findicons.com.  This is a great site because it includes a lot of different icon sets, as well as information on the copyright and licensing information for each icon you use - just make sure to include the credits for the icons you do use in your code.

Remember the __credits__ variable in your module?  We'll use that to add our icon credits as we use them.

For instance, I saved this icon's 16x16 version as nexsys/resources/img/file_new.png, and now my __credits__ variable in my resources module looks like:

__credits__     = [
('Oxygen Team', 'New Text File Icon (GNU/GPL from findicons.com)'),
]

As I add icons, I make sure that I am legally allowed to use them, and add the credits to the list as such.  Doing things this way will let us pool the credit information through code later when generating documentation and the About credits as we'll show in the future.

At this point, feel free to browse through for some icons for your actions.  I've found the best looking sizes for actions to be 16x16 icons.  Make sure to save them within your resources module though so you can reference them later, and to add the author and description to your credits.

This is a list of the icon file names that I've saved out to use - you don't need to name them the same way, but if you change them, remember the differences as we'll reference them later (all of these icons were saved in nexsys/resources/img/)
  • about.png
  • copy.png
  • cut.png
  • delete.png
  • dir_new.png
  • file_new.png
  • find.png
  • go_back.png
  • go_home.png
  • go_up.png
  • paste.png
  • quit.png
  • refresh.png
  • settings.png
  • show_brief.png
  • show_details.png
  • tab_close.png
  • tab_copy.png
  • tab_new.png
  • terminal.png
  • vertical_mode.png
And the end result of my credits ends up looking like this:

__credits__     = [
('Oxygen Team',         'New Text File Icon (GNU/GPL from findicons.com)'),
('Ruby Software',       'New Folder icon (Freeware Link Required: '\
                    'http://findicons.com/icon/26211/opened_folder?id=26224)'),
('David Vignoni',       'Quit Icon (GNU/GPL from findicons.com)'),

('Mark James',          'Cut Icon (Creative Commons from findicons.com)'),
('Silvestre Herrera',   'Copy Icon (Unknown License from findicons.com)'),
('FatCow Web Hosting',  'Paste Icon (Creative Commons from findicons.com)'),
('GNOME icon artists',  'Delete Icon (GNU/GPL from findicons.com)'),

('Yusuke Kamiyamane',   'Details Icon (Creative Commons from findicons.com)'),
('Yusuke Kamiyamane',   'Brief Icon (Creative Commons from findicons.com)'),
('Jack Cai',            'Refresh Icon (Creative Commons from findicons.com)'),

('PixelMixer',          'Go Back Icon (Freeware Link Required: '\
                    'http://findicons.com/icon/2991/left?id=3033'),
('PixelMixer',          'Go Home Icon (Freeware Link Requird: '\
                    'http://findicons.com/icon/2959/home'),
('PixelMixer',          'Go Up Icon (Freeware Link Required: '\
                    'http://findicons.com/icon/2956/up'),
                        
('schollidesign',       'Find Icon (GNU/GPL from findicons.com)'),
('Yusuke Kamiyamane',   'Terminal Icon (Creative Commons from findicons.com)'),
('Oxygen Team',         'Settings Icon (GNU/GPL from findicons.com)'),

('David Vignoni',       'Add Tab Icon (GNU/GPL from findicons.com)'),
('David Vignoni',       'Close Tab Icon (GNU/GPL from findicons.com)'),
('David Vignoni',       'Copy Tab Icon (GNU/GPL from findicons.com)'),
('FatCow Web Hosting',  'Vertical Icon (Creative Commons from findicons.com)'),

('Prathyush',           'Help Icon (Creaive Commons from findicons.com)'),
('PixelMixer',          'About Icon (Freeware Link Required: '\
                    'http://findicons.com/icon/2984/info?id=3026'),
]

Important: This is not necessary the legal way that your tool should be structured!  Make sure to check the licensing requirements for your application (especially if it is for commercial or public use) to make sure that you can mix and match the licenses as I've done - this is for tutorial purposes and so I did not concern about making sure the legal licenses match!

Adding Icons to the UI

Now that we've gone through and found all these different icons, we can start using them in our Designer interface.  As I mentioned above - because the PyQt system can't dynamically load QResource instances, we're just going to path directly to our icons.

This will end up saving out a relative path to our resource file, so from our main window in nexsys/gui/ui adding the 'file_new.png' icon will be saved as ../../resources/img/file_new.png in the UI file, which we'll be able to leverage when loading through code.

For now, click on the 'ui_newfile_act' in your Object Browser in the Qt Designer.  In the Property Editor, scroll down until you see the icon property.  When you go to edit it, clicking on the tool button will prompt you to locate an icno based on a loaded resource - we're going to skip that.  Next to the button is a down facing arrow that will popup a menu, from here you can pick 'Choose File'.  Pick that, and assign your file_new.png icon to the action.

If you now click on your File menu in the designer, you'll see the icon you downloaded linked to your action.

Go ahead and do this for the following combination of action and icon combos.  One thing to note about this system - make sure that the icons were saved to your resource folder since we're going to be working with relative paths.
  • 'ui_newfile_act'                > file_new.png
  • 'ui_newdir_act'                > dir_new.png
  • 'ui_quit_act'                     > quit.png
  • 'ui_boardcut_act'            > cut.png
  • 'ui_boardcopy_act'          > copy.png
  • 'ui_boardpaste_act'        > paste.png
  • 'ui_deletefile_act'            > delete.png
  • 'ui_viewdetails_act'         > show_details.png
  • 'ui_viewbrief_act'            > show_brief.png
  • 'ui_viewreload_act'         > refresh.png
  • 'ui_goback_act'               > go_back.png
  • 'ui_goup_act'                   > go_up.png
  • 'ui_gohome_act'              > go_home.png
  • 'ui_search_act'                > find.png
  • 'ui_filefind_act'                > find.png
  • 'ui_terminal_act'              > terminal.png
  • 'ui_terminalhere_act'      > terminal.png
  • 'ui_configshortcuts_act' > settings.png
  • 'ui_configtools_act'         > settings.png
  • 'ui_configapp_act'           > settings.png
  • 'ui_tabnew_act'               > tab_new.png
  • 'ui_tabcopy_act'              > tab_copy.png
  • 'ui_tabclose_act'             > tab_close.png
  • 'ui_verticalmode_act'     > vertical_mode.png
  • 'ui_helpabout_act'          > about.png
So, that may seem like a little bit of work - but icons (and good icons) can really improve a user's experience with your tool and is definitely worth it.  Once you get comfortable working with the designer, all of these steps will take about 45 minutes to get a pretty complex and robust interface built with it - the longest time spent finding icons.  So this is actually a very good way to prototype your tools for user approval as well.

If you run a preview of your tool now, you'll see that you have icons in your menu items.

Toolbars

Another feature of the QMainWindow is its native support for toolbars.  A QToolBar is a specialized widget that allows you reuse actions as buttons for your interface (as well as other widgets), also giving the user the ability to control where on the window the toolbar resides and whether or not it is floating or docked.

Now that we have our icons assigned to our actions, we can go ahead and create a toolbar for them.

If you Right-Click on the Main Window and choose Add Tool Bar, you'll see a new widget appear under your menu.  Rename the new toolbar to 'ui_main_tools' in the Object Browser.

Now, if you go to the Action Editor (which should be tabbed on the bottom right-hand side of the window by default), you'll see a list of all the actions that you have created on your window.

Go through and find the 'ui_goback_act' and drag & drop it onto your toolbar.  You should now see a new button with the action's icon on it.

Actions are very, very handy in Qt.  They allow you to control multiple input points from a user and route them through to a single slot via a single connection.  Instead of having to link to both the menu item and to a button, the tool button will actually emit the same signal as the menu item for your action.  This way, whether a user clicks the action in the menu, or clicks on the buttons - you're only working with 1 connection in code.

Go ahead and drag on the 'ui_goup_act' and 'ui_gohome_act' actions as well.

Lets now create a little visual separator before adding other actions to create visual groups for the user.  Right click on your toolbar and choose Append Separator.  This will add a vertical bar to split visually your icons around.

Lets now add the 'ui_search_act', 'ui_newfile_act', 'ui_newdir_act', 'ui_deletefile_act' actions, then another separator, and then the 'ui_boardcut_act', 'ui_boardcopy_act', and 'ui_boardpaste_act' actions after that.

We have now given the user a handy little default toolbar for them.

Next up, lets setup som properties on the toolbar.  By default, the toolbar is trying to use 24x24 icons, but we're only going to need 16x16.  If you select the 'ui_main_tools' toolbar in the form or Object Browser, scroll down and set the iconSize to 16x16.  You can also control other user options here such as if they should be able to float the toolbar, and where on the window they should be allowed to dock it, in case you want to control that.  Let's also set its windowTitle property to 'Main Toolbar'.

This way when a user right clicks on the window, they'll see that in a list of togglable toolbars with a more pleasing name.

Shortcuts

Right now, a user can select the New Text File option from the menu, or click on the button - but what if a user wants to hit Ctrl+N to create it as well?

The other thing that actions can do is handle all of your hotkeys and shortcuts for you as well.  Using the QAction in conjunction with a QKeySequence allows you to control a users hotkey inputs to trigger an action.

In Designer, you can assign a hotkey to an action by selecting, scrolling down to the shortcut property, and in the value field hit the key combination you want to assign.  Be warned though - while you're editing, it will record all keystrokes - so you cant hit Tab or Escape or Ctrl+S in it to get out, it'll just add that to the list of hotkey options.  So after you click on the value, then click your hotkey, then click off the property in the tree widget to set the value.

So, if you select 'ui_newfile_act' and scroll to the Shortcut property in the Property Editor, click the Value column and hit Ctrl+N, then click off it.

Thats it!  Now, if the user hits Ctrl+N in your application - it will automatically trigger your action!

How easy is that?  3 different entry points to your action, and you only need to set it up in the Designer.

At this point - feel free to go through and assign hotkeys to your different actions that you'd want to use as a pretty good default.  As a guideline - try to follow common standards - there's a full wiki page on shortcut standards that I shared in the PyQt Coding Standards page.  This helps users keep consistent motions between applications.  If you preview your window again, you'll also notice that the hotkey's that you've assigned to each action will be displayed in the Menu for the user - another perk of using the native hotkey system.

Overall - your window should now look something like this:



Review of Actions

So, as you've seen - Actions are a very integral part of the Qt system.  When you setup your application to use actions, you are opening the door for a lot of options.  With our code setup this way to start with, it will be easy to let the user configure their preferences for how they want their toolbar structured, and what hotkey preferences they want to use,  without us having to write much specialized code.

I'd definitely recommend getting into the habit of Actions as soon as possible - I didn't really take advantage of all they had to offer for a number of years when I was developing in Qt because I didn't fully understand them - and I now know I could have saved a lot of time and effort building my own systems had I just spent the time to figure actions out properly.

Coming up Next

We're finally going back into the code!  At this point, we have menu's, tool bars, a number of widgets, and few connections all setup with the Designer.

Now lets get in there and put them to work.

5 comments:

  1. For other folks following this tutorial but using PySide: The loadUI() function from the previous tutorial will fail, in the context of this package's directory structure, to load icons that have been added in the manner above. Here is my loadUI() code that accounts for this.


    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


    P.S.: Setting the path here would also be necessary for any other resources mentioned in the .ui file to load correctly. not just the icons.

    ReplyDelete
  2. Hey Christopher,

    I need to start writing again on this as I've picked up a lot of new techniques and have written a number of useful open source libraries since this tutorial set....but one such package that I wrote and would recommend looking into is:

    https://github.com/ProjexSoftware/xqt/

    This package allows you to abstract out your development from PyQt or PySide and have it work seemlessly with both. I still need to write up more documentation on it, but have a look, it may help you out!

    Eric

    ReplyDelete
  3. Wow, that's going to come in handy, especially with your other LGPL widget collection. Thanks!

    ReplyDelete