Saturday, October 15, 2011

Nexsys: Main Window Ui (Part II)

Now that we have our menu's and actions flushed out, we can start adding in the main components to our central widget.

Creating the Panels

As I mentioned in the last post,  we are actually going to create a separate widget for the contents of our panels - so we're just going to block them in using a QTabWidget right now.

The general idea when breaking your widgets up into more maneagable classes is to try to identify where duplication is going to occur.  Since we want to have tabbed access to multiple filepaths in our browser, and our side-by-side panels will be identical from a development standpoint, we wouldn't want to create two panels in our main widget and then duplicate the code to manage them.

Instead, we'll create 1 class, and instantiate it as many times as we need to - and simply use our main window to handle how the panels communicate with each other.

The main ways that I use for this type of blocking is to first think about how many of each sub-widget I'm going to need - in this case, since we're going to let the user have any number of panels, a tab widget container best fits our usage.

In another case, we may only want the user to have a single, set panel for something (say like a layer manager for a calendar) but we would still want to define the widget outside of the main window.  In those cases, I would either use a QScrollArea if I want the contents to be able to stretch beyond the window's height restrictions and then use the QScrollArea.setWidget method to assign my custom widget to the contents of the area, or use the Widget Promotion feature of Designer.  These concepts are a little more advanced, so we won't go into them for this example, but if you search the blog for 'Widget Promotion' you'll find the topics that covert that.

The next thing I think about is how I want the user to interact with each panel.  If I knew I needed set sizes, I would use a QLayout to structure the contents.  In our current case however, I want to let the user alter the size of each widget - so I want to use a QSplitter instead.

So, lets start out with our panel containers - lets drag and drop 2 QTabWidget classes from the Widget Box > Containers > Tab Widget onto our window.

Rename the left widget to 'ui_left_tab' and the right one to 'ui_right_tab'.

Now, select both, and choose the 'Lay Out Horizontally in Splitter' option from the Form toolbar.  This will create a horizontal splitter for our tab widgets in our window.

Now, while normally laying out widgets in a form layout will generate a QLayout class, and we don't need to worry about renaming them - when using the QSplitter, its a little different, because the QSplitter actually inherits from QWidget and not QLayout.

So go ahead and rename the new splitter widget to 'ui_main_split', since we're going to want to access this later on.  For now, you can go ahead and just leave the splitter floating somewhere in the middle of your widget - we'll get to it later.

Creating the Command Line Widget

The next component we're going to add to the main window is our command line execution widget.  This widget will contain a label that will display the current path that the command will be executed in, and a combobox that will allow a user to enter in command options that can be remembered and re-executed.

One way to create this, would be to create a label and a combobox and lay them out horizontally - which, is perfectly fine.  If you remember from the last tutorial though, we created a 'Show Command Line' action.  What we're going to do with this action is link it to toggling the visible state of this widget.  Laying the widgets out on the main window means that each widget is treated as a separate widget.

To make life a little easier on ourselves, lets actually group these two widgets together inside another widget instead of directly on the main window.  That way, we can use the hierarchy information to affect just the parent widget much easier.

So, first, drag in a QWidget from the Widget Box > Containers > Widget and place it towards the bottom of your window.  This will just create a blank container for us.  Resize it so it stretches about the width of our window, and rename it to 'ui_cmdline_widget'.

We'll now put our label and combo box inside this widget, so first drag in a QLabel from Widget Box > Display Widgets > Label and drop it ontop of our command line widget, towards the left side of the widget.  Rename it 'ui_cmdline_lbl'

Next, drag and drop a QComboBox from Widget Box > Input Widgets > Combo Box and drop it ontop of our command line widget, on the right hand side of our label.  Rename this combobox to 'ui_cmdline_combo'.

Finally, select our command line widget and hit the 'Lay Out Horizontally' button from the Form toolbar to layout our widgets horizontally, and set the layout for the command line widget.

A note about layouts: once you have your widgets parented inside of a container, and layed out roughly the way you want them (left-to-right, top-to-bottom, or in a grid), you can just select the parent widget and apply one of the layout options to it to both layout the widgets, and assign the layout to the widget.  If you first had set the label and combobox in a horizontal layout, and then applied a layout to the parent widget - you'd actually double up your layouts unnecessarily.

At this point, your window should look roughly like this:

Now, we could have separated the command line widget out as a completely separate class, and then assigned it to our parent window instead of the way we have just done it - and that would have been perfectly fine as well.

The general logic I use when deciding if I should separate the component into a separate widget class entirely is to think about
  1. The complexity of the parts - if there are a lot of widget and creates a lot of custom logic, it may benefit from its own class.
  2. The duplication - if I intend to use the same logic for two or more different widgets within a parent window (like you'll see with the panel classes), then I would create a class and reuse the class.
  3. The reusability - if the widget can be generalized and be useful in other windows or applications, then I would make a library class and design it so that it doesn't need to know about what window its in, and just provde api level methods to interact with it from the window.
In this project - we'll probably hit all 3 cases, so you'll be able to see an example of each line of reasoning.  For now, this case is not complex, duplicated, nor really that reusable - so I am ok with writing its logic into the main window's class.

Creating the Functions Buttons

The next widget we're going to make is the functions toolbar.  This is going to be at the bottom of our widget and provide our user with some buttons to press that will call some of our actions.

For this widget, we're going to create some buttons with custom text on them - so we are going to bypass the standard toolbar option and manually create the buttons.

So lets create another QWidget underneath our command line widget, and call it 'ui_funcs_widget'.

Inside that widget, drag 9 different QPushButton objects from Widget Box > Buttons > Push Button, layed out left to right

Label them in order:
  1. Term, rename to 'ui_functerm_btn'
  2. View, rename to 'ui_funcview_btn'
  3. Edit, rename to 'ui_funcedit_btn'
  4. Copy, rename to 'ui_funccopy_btn'
  5. Move, rename to 'ui_funcmove_btn'
  6. Mkdir, rename to 'ui_funcdir_btn'
  7. Delete, rename to 'ui_funcdelete_btn'
  8. Rename, rename to 'ui_funcrename_btn'
  9. Quit, rename to 'ui_quit_btn'
Now, select the root funcs widget and hit the 'Lay Out Horizontally' form option again to lay these buttons out horizontally.

At this point, your form should look roughly like this:

Finishing the Layouts

Now we have all the pieces for our main application - albeit unorganized on our form.

If you've been following along, when you look at your centralwidget in the Object Inspector, the three top-level widgets should be 'ui_cmdline_widget', 'ui_funcs_widget', and 'ui_main_split', and they should be roughly laid out vertically.

If this is the case - then to finalize the widget layout, you simply need to select your root window and hit the 'Lay Out Vertically' Form layout option to layout the widgets vertically.

One thing you'll notice is that each of our components takes up the same percentage of space.  This is not what we want at all, we want the panel widgets to take up as much space as possible in our form, with the command line and function buttons to take up as little space towards the bottom of our form as possible.

Stretching the Splitter

Up until now, we've only looked at spacers as a means to push our widgets around within layouts.  However, there are many cases where we don't want to push the widget, but we want a particular widget (or widgets) to maximize the amount of used space.

This is where the QSizePolicy comes into play.  Each widget can define its own sizing information that layouts will use to determine how to distribute the resizing information.

To edit the size policy for a widget in Designer, its in the Property Editor panel on the right-hand side.  Select the 'ui_main_split' in either the Object Inspector or in your form itself and scroll the Property Editor down (or up) until you find the sizePolicy property.

Here there will be a couple of options and if you want to learn more about it, I suggest reading the Qt documentation on sizing.  The properties we're looking for are the Horizontal Policy and Vertical Policy.  These policy types will determine how a QLayout will attempt to resize the contents for a particular widget.

You'll see that by default, a QSplitter that was created with a horizontal alignment will be set to Expanding for its Horizontal Policy, and Preferred for its vertical policy.  The Expanding option will let Qt know that it should maximize its size, the Preferred option will defer its sizing to the contents of its widgets - and in this case, our QTabWidget's also differ - but to no children, so they will try to minimize the amount space that is used.  To make sure that our panel contents take up as much vertical space as possible, we need to just switch the Vertical Policy from Preferred to Expanding.

You'll now see the tab splitter stretch to use as much vertical space as is available.

Stretching the Command Line

Now that we've learned a little about size policies - we can turn our attention to the combobox that we've created in our command line widget.  At the moment, the label and combobox are each taking up half the width of the widget, when we really want to have the combobox take up as much space as possible.

If we select our 'ui_cmdline_combo' widget, we see it has a Horizontal Policy of Preferred and a Vertical Policy of Fixed.  In this case, we're fine with the vertical policy as it is - but want to take as much horizontal space as possible - so lets switch the Horizontal Policy from Preferred to Expanding.

There we go...we're starting to look good.

Removing Extra Spacing

Ok, so now we have our widgets stretching the way we want, and generally sizing well.  But what is up with all the excess blank space?

If you preview your widget as it is,  you'll notice a lot of unnecessary space between the panels and the command line, and the comand line and buttons, and between the buttons themselves.

As a developer/designer, you should try to focus on the real estate of space for your tool.  Generally, users don't need or want 'fluff' in their applications.  They have limited screen real estate, so a tool needs to be visually uncluttered, but also minimalist in terms of the space it does use - there should be exactly the number of widgets a user will need to use - no more, no less.  Of course, if you ever figure that number out for every user...let me know.

By default, Qt's layouts will try to add on padding to make layouts space things out in a visually pleasing way - usually a value of 9px for each margin, and 6px for spacing between items.

What happens when you get into more complex layouts though, those paddings can double up, as we're seeing here.

The solution to this, is to force the margins and spacing values out for the layouts that we don't want them in.  In this case, we want to clear the margins for all of our layouts.  So lets start with the command line and functions widgets.

If you select both the 'ui_cmdline_widget' and 'ui_funcs_widget' using the Ctrl key, you will see in the Property Editor that you can still edit properties.  Qt allows you to edit common properties between multiple selected widgets - so if you have 2 QLineEdit's selected, all the properties of a line edit and all inherited classes will be editable - if you select a QLineEdit and a QWidget, then only the QWidget properties would be editable.

With both widgets selected, scroll down until you see the Layout section.  You'll see that the layouts still have their default margin and spacing values of 9 and 6.  Lets just change those all to 0's.  You can see quickly how the unnecessary blank space is being removed.

Lastly, lets select the 'centralwidget' and do the same thing, but lets keep a little spacing, so set the layoutSpacing value to 6 and the margins to 6 (or whatever value looks pleasing to you).  I also set the bottom margin value to 0, since we have the status bar using some space at the bottom of our window.

Go ahead and run a preview of your dialog now and you will get a working example of the tool, or save the file and run your nexsys application.  Without any code change, we now have a main interface for our application!.

Coming up Next

Now that we have the overall structure for our application working, we're going to need to still setup a few more options in our ui file.  We'll make a few default connections and tweak some of the action properties to work for what we need.

No comments:

Post a Comment