Laying It All Out, Part 1
Besides font sensitivity it is also tedious to have to explicitly design a GUI with precise locations and sizes for each component. It makes adding new components or moving things around more difficult than it should be.
The solution to these problems is something called a layout manager. What a layout manager does is manage laying out all the components of your application based on various criteria such as the type of layout as well as the minimum, preferred and maximum sizes of the GUI components. If the user resizes the window or changes the font, the layout manager gracefully resizes and realigns all the components under its management.
As implied in the first paragraph, BeOS never had a built-in layout management system. Several third parties did come to the rescue, such as Marco Nelissen with his award winning liblayout, as well as Brian Tietz in his Santa's Gift Bag library and Angry Red Planet in their ARPCommon library. Of course while these systems all achieved the same basic goal, their implementation and usage was quite different. "Porting" an application from one layout system to the other would be fairly difficult.
Fortunately for us, Ingo Weinhold came to the rescue by committing a layout management system into the Haiku source tree back in August 2006. It has been tweaked quite a bit since then but so far has not been put to much use. I figure that is because not many know about it. Ingo is a busy man, so I decided to learn the layout system and write these articles.
This first article will describe the layout system from a higher level, and then in one or more future articles I will go into more detail.
First things first, Ingo fully admits that much of the design of this layout system was inspired by the QLayout and friends in the Qt library. So it might be helpful to read about that too, though I hope to tell you everything you need to know to work with Haiku's classes eventually.
Types of Layout Managers
I will start by describing the various types of layout managers that are currently included in Haiku and how they layout the views under their control:
- BCardLayout: the contained views are layed out like a deck of cards, one on top of the other and all in the same area, and only one can show at a time. This would be useful in implementing a tab-based interface for showing multiple documents in one window, or for settings windows with multiple sections (like in the settings for BeIDE or FireFox.)
- BGridLayout: the contained views are layed out in a grid, with each view getting the same general amount of space (though there are options to allow a view to span multiple rows and/or columns.) This would be useful in implementing a thumbnailing application for example, with each thumbnail getting it's own equal sized space.
- BGroupLayout: the contained views are layed out in a group, either horizontally or vertically, depending on what orientation is wanted for the BGroupLayout. When used in combination (multiple horizontal BGroupLayouts contained in one vertical BGroupLayout for example) this is the most flexible and useful of the layouts. I imagine many future Haiku applications will make much use of this layout class.
- BSplitLayout: this acts much like a BGroupLayout, except each view is separated by a splitter view that allows its contained area to be resized. Also optionally the area can be resized such that the view disappears completely, something called collapsing. You won't ever actually use this layout directly, but only through the BSplitView class.
- BALMLayout: the newest kid on the block, this was added in February 2008 from code created by Christof Lutteroth and James Kim. ALM stands for Auckland Layout Model, which is a new kind of layout invented by Christof at The University of Auckland. More information about ALM can be read on its SourceForge project page. I suspect I will dig into this layout more in the future and may dedicate an entire article to it. But the previous link provides a lot of information about the concepts behind ALM.
How To Build a GUI Using a Layout
To build a Haiku GUI that uses the layout system, you first need to figure out what kind of layout manager or managers you might need. For an existing GUI it is probably fairly obvious from what you can see on the screen. For a new GUI it would probably be helpful to draw it out, though this would probably be done even if there was no layout system involved.
Once you know the kind of layouts you plan to use, you need to make sure all the BViews you are using have the needed methods to be used in the layout system. All of the Haiku views and controls should already be compatible with the layout system (and if they are not, log a bug.) For custom BViews some extra work is required, which I will explore in a future article. For the purposes of this article I will only use views that are already well-supported by the layout system.
For the purposes of an example I could put together a simple example GUI, but I think it will be more instructive to show an actual GUI that already exists in Haiku that is not font sensitive. The E-mail Preferences applet shown above is one example, but I don't want the complication of its GUI to interfere with showing how the layout system works. Luckily I have another good option in the MidiPlayer app. See Figure 2.
So based on the GUI in Figure 2, what layout managers will we need? Well the first thing I see is that there are essentially four sections arranged vertically:
- The "Drop MIDI file here" control.
- The labels and controls in the middle.
- A separator.
- The Play button.
So this means that the highest-level layout manager should be a vertical BGroupLayout. The set of labels and controls seems to be arranged in a grid, so for that section we can have another layout manager, a BGridLayout. But before I start showing the code for creating this layout, let's look at the original GUI creation code. It is contained within MidiPlayerWindow.cpp in the CreateViews() method. It also makes use of a few macros. The relevant code (with line numbers) as of r26407 is show below:
32 #define _W(a) (a->Frame().Width()) 33 #define _H(a) (a->Frame().Height()) ... 240 void MidiPlayerWindow::CreateViews() 241 { 242 scopeView = new ScopeView; 243 244 showScope = new BCheckBox( 245 BRect(0, 0, 1, 1), "showScope", "Scope", 246 new BMessage(MSG_SHOW_SCOPE), B_FOLLOW_LEFT); 247 248 showScope->SetValue(B_CONTROL_ON); 249 showScope->ResizeToPreferred(); 250 251 CreateInputMenu(); 252 CreateReverbMenu(); 253 254 volumeSlider = new BSlider( 255 BRect(0, 0, 1, 1), "volumeSlider", NULL, NULL, 256 0, 100, B_TRIANGLE_THUMB); 257 258 rgb_color col = { 152, 152, 255 }; 259 volumeSlider->UseFillColor(true, &col); 260 volumeSlider->SetModificationMessage(new BMessage(MSG_VOLUME)); 261 volumeSlider->ResizeToPreferred(); 262 volumeSlider->ResizeTo(_W(scopeView) - 42, _H(volumeSlider)); 263 264 playButton = new BButton( 265 BRect(0, 1, 80, 1), "playButton", "Play", new BMessage(MSG_PLAY_STOP), 266 B_FOLLOW_RIGHT); 267 268 //playButton->MakeDefault(true); 269 playButton->ResizeToPreferred(); 270 playButton->SetEnabled(false); 271 272 BBox* background = new BBox( 273 BRect(0, 0, 1, 1), B_EMPTY_STRING, B_FOLLOW_ALL_SIDES, 274 B_WILL_DRAW | B_FRAME_EVENTS | B_NAVIGABLE_JUMP, 275 B_PLAIN_BORDER); 276 277 BBox* divider = new BBox( 278 BRect(0, 0, 1, 1), B_EMPTY_STRING, B_FOLLOW_ALL_SIDES, 279 B_WILL_DRAW | B_FRAME_EVENTS, B_FANCY_BORDER); 280 281 divider->ResizeTo(_W(scopeView), 1); 282 283 BStringView* volumeLabel = new BStringView( 284 BRect(0, 0, 1, 1), NULL, "Volume:"); 285 286 volumeLabel->ResizeToPreferred(); 287 288 float width = 8 + _W(scopeView) + 8; 289 290 float height = 291 8 + _H(scopeView) 292 + 8 + _H(showScope) 293 + 4 + _H(inputMenu) 294 + _H(reverbMenu) 295 + 2 + _H(volumeSlider) 296 + 10 + _H(divider) 297 + 6 + _H(playButton) 298 + 16; 299 300 ResizeTo(width, height); 301 302 AddChild(background); 303 background->ResizeTo(width, height); 304 background->AddChild(scopeView); 305 background->AddChild(showScope); 306 background->AddChild(reverbMenu); 307 background->AddChild(inputMenu); 308 background->AddChild(volumeLabel); 309 background->AddChild(volumeSlider); 310 background->AddChild(divider); 311 background->AddChild(playButton); 312 313 float y = 8; 314 scopeView->MoveTo(8, y); 315 316 y += _H(scopeView) + 8; 317 showScope->MoveTo(8 + 55, y); 318 319 y += _H(showScope) + 4; 320 inputMenu->MoveTo(8, y); 321 322 y += _H(inputMenu); 323 reverbMenu->MoveTo(8, y); 324 325 y += _H(reverbMenu) + 2; 326 volumeLabel->MoveTo(8, y); 327 volumeSlider->MoveTo(8 + 49, y); 328 329 y += _H(volumeSlider) + 10; 330 divider->MoveTo(8, y); 331 332 y += _H(divider) + 6; 333 playButton->MoveTo((width - _W(playButton)) / 2, y); 334 }
I am not going to walk through the code, but the main thing I want noticed is all the hard-coded numbers used in the manual layout, and all the work required to get things aligned. Another benefit to the Haiku layout system is cleaner code, and I think this example should prove that well. In fact I think I will let the code speak for itself. Here is the above CreateViews() method rewritten to use the layout system:
246 void MidiPlayerWindow::CreateViews() 247 { 248 // Set up needed views 249 scopeView = new ScopeView; 250 251 showScope = new BCheckBox( 252 BRect(0, 0, 1, 1), "showScope", "Scope", 253 new BMessage(MSG_SHOW_SCOPE), B_FOLLOW_LEFT); 254 showScope->SetValue(B_CONTROL_ON); 255 256 CreateInputMenu(); 257 CreateReverbMenu(); 258 259 volumeSlider = new BSlider( 260 BRect(0, 0, 1, 1), "volumeSlider", NULL, NULL, 261 0, 100, B_TRIANGLE_THUMB); 262 rgb_color col = { 152, 152, 255 }; 263 volumeSlider->UseFillColor(true, &col); 264 volumeSlider->SetModificationMessage(new BMessage(MSG_VOLUME)); 265 266 playButton = new BButton( 267 BRect(0, 1, 80, 1), "playButton", "Play", new BMessage(MSG_PLAY_STOP), 268 B_FOLLOW_RIGHT); 269 playButton->SetEnabled(false); 270 271 BBox* divider = new BBox( 272 BRect(0, 0, 1, 1), B_EMPTY_STRING, B_FOLLOW_ALL_SIDES, 273 B_WILL_DRAW | B_FRAME_EVENTS, B_FANCY_BORDER); 274 divider->SetExplicitMaxSize( 275 BSize(B_SIZE_UNLIMITED, 1)); 276 277 BStringView* volumeLabel = new BStringView( 278 BRect(0, 0, 1, 1), NULL, "Volume:"); 279 volumeLabel->SetAlignment(B_ALIGN_LEFT); 280 volumeLabel->SetExplicitMaxSize( 281 BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); 282 283 // Build the layout 284 SetLayout(new BGroupLayout(B_HORIZONTAL)); 285 286 AddChild(BGroupLayoutBuilder(B_VERTICAL, 10) 287 .Add(scopeView) 288 .Add(BGridLayoutBuilder(10, 10) 289 .Add(BSpaceLayoutItem::CreateGlue(), 0, 0) 290 .Add(showScope, 1, 0) 291 292 .Add(reverbMenu->CreateLabelLayoutItem(), 0, 1) 293 .Add(reverbMenu->CreateMenuBarLayoutItem(), 1, 1) 294 295 .Add(inputMenu->CreateLabelLayoutItem(), 0, 2) 296 .Add(inputMenu->CreateMenuBarLayoutItem(), 1, 2) 297 298 .Add(volumeLabel, 0, 3) 299 .Add(volumeSlider, 1, 3) 300 ) 301 .AddGlue() 302 .Add(divider) 303 .AddGlue() 304 .Add(playButton) 305 .AddGlue() 306 .SetInsets(5, 5, 5, 5) 307 ); 308 }
The only significant changes in the first section of the method where the views are created is the removal of the various manual layout related method calls, like playButton->ResizeToPreferred();
, which was originally on line 269. There have been a few additions to give the layout system some hints on how we want things laid out. For example an unlimited width and max height of 1 is set up for the divider BBox, so it stays looking like a divider. In addition the volumeLabel is set up so it aligns left and fills up all the space available. This keeps it aligned with the labels for the BMenuFields above it.
Next we have the real "meat" of the layout code which starts on line 284. The first thing I do is set a base layout. In this case I have decided to use a sort of "dummy" horizontal group layout so that I can later use the group layout builder to create the vertical layout I really want. It seems to be a quirk of the layout system that you have to have a layout manager set up in the window before you start adding other views that are managed by the layout system. I probably could have created the main vertical group layout outside of the builder and made that the root layout, but I don't think the code would have looked as nice.
Speaking of the layout builders, they were created specifically for uses like the above, with each method returning an instance of the builder allowing essentially unlimited method chaining. With the right indentation it can look quite nice and becomes a good represention of how the GUI is structured. Hence why I like to use them. Currently there is a GridLayoutBuilder, GroupLayoutBuilder, and SplitLayoutBuilder.
On line 286 we have an AddChild() call with the call to the layout builders inside of it. The first argument for the BGroupLayoutBuilder is the orientation of the group layout and the second argument is the spacing used between the layout items. This gives the views contained within the layout a little breathing room so they don't look all crammed together. Once we have the group layout builder we can start adding views to it with the Add() method, and the first thing added is the scope view. Then we have the middle section which we want managed by a grid layout, hence the call to the grid layout builder. The arguments to BGridLayoutBuilder are also for spacing, in this case the horizontal and vertical spacing, respectively.
On line 289 we start adding views to the grid layout by adding something called glue. What is glue? It is basically a filler that takes up space in the layout so that the views we care about are aligned properly. In this case I want the showScope checkbox aligned with the menus of the menu fields below it. More glue is used below in the vertical group layout to provide some space between the grid layout, the divider, the play button and the bottom of the window. This keeps things looking nice and also helps us maintain the look of the original MidiPlayer GUI.
The other arguments to the Add() method for the BGridLayoutBuilder is the column and row we want the given view to appear in. So from this it can be seen that the glue is in column 0, row 0, the showScope checkbox is in column 1, row 0, the reverb menu label is in column 0, row 1, etc. Speaking of the reverb menu item, we can see that there are special methods in the BMenuField class to produce separate layout items for the one view. This is an interesting aspect of the layout system in that views that consist of different components can actually have their separate sections managed individually by the layout system. For another example of this look at the code for the ActivityMonitor, which uses this technique for the graph and legend of the main activity view.
The final call to the group layout builder is SetInsets(), which basically is used to put some padding around the edge of the layout, in the order of left, top, right, and bottom. Five pixels seems to be a pretty standard choice for the insets. I could have used 8 pixels which would have been like what the original GUI had, but I think 5 is fine.
There were a few other code changes needed to get the final GUI shown in Figure 3. First the needed headers were added. Second the constructor for the MidiPlayer window needed the B_AUTO_UPDATE_SIZE_LIMITS so that it gets resized automatically by the layout system:
41 MidiPlayerWindow::MidiPlayerWindow() 42 : BWindow(BRect(0, 0, 1, 1), "MidiPlayer", B_TITLED_WINDOW, 43 B_ASYNCHRONOUS_CONTROLS | B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS)
Third the methods used to build the menu fields needed to be changed to use the simple layout system friendly contructor. Also any manually layout code was removed. So for example the CreateReverbMenu() was changed so that this:
230 reverbMenu = new BMenuField( 231 BRect(0, 0, 128, 17), "reverbMenu", "Reverb:", reverbPopUp, 232 B_FOLLOW_LEFT | B_FOLLOW_TOP); 233 234 reverbMenu->SetDivider(55); 235 reverbMenu->ResizeToPreferred();
became this:
235 reverbMenu = new BMenuField("Reverb:", reverbPopUp, NULL);
A similar change was made for the CreateInputMenu() method.
Finally the constructor for the ScopeView was changed to make it a bit wider to accomodate a size 18 font. This was the "cheap" way to do it but in this case it works fine. The "proper" way would be to add the methods needed by the layout system to tell the layout system what the minimum, preferred and maximum sizes would be for that view. For this article I did not want to cover that but will probably go into it in a future article on the layout system.
To see all of this updated code take a look at the MidiPlayer sources which are in the Haiku repository at src/apps/midiplayer. To see the previous code go back to before revision 26408 when I submitted these changes.
Conclusion
To get more ideas and tips on how to use the layout system until more documentation is written take a look at Ingo's test application for the layout system, called LayoutTest1. The source for this is at tests/kits/interface/layout/LayoutTest1.cpp and it can be tested in Haiku by adding the following line to your UserBuildConfig file:
AddFilesToHaikuImage home config bin : LayoutTest1 ;
It can then be run either from Tracker from /boot/home/config/bin or by running it from Terminal. The code also has various examples of how to build a GUI with the layout system, sometimes using builders and sometimes not, depending on what is appropriate.
In addition the code for all the layout classes is in the Haiku Interface Kit, so for those of you who really want to dig into it, take a look at the Interface Kit headers in headers/os/interface and the implementation in src/kits/interface and src/kits/interface/layouter.
Finally a warning: because these classes are new and untested from the perspective of an API, they are considered a private API and for now should only be used inside Haiku core code, such as the included applications or preference applets. This is because the API might change or need to be tweaked and we would not want to break other people's code (we can fix broken code inside the Haiku repository ourselves.) So just keep that in mind and use the layout system classes at the risk of having your code broken.