Image of Navigational Map linked to Home / Contents / Search ID32 - (Resolution) Independence Day

by Peter Wone - Wombat and Me
Image of Line Break

There is no easy "one sizer fits all" solution to this thorny perennial, but this issue it's my pleasure to present two simple techniques which, used in tandem, can sort out many resizing problems.

The idea is that you nest container objects in a container that manages their sizes and position, tiling them horizontally depending on which Resize handler you choose for the outermost container, and the inner containers are responsible for stretching their children according to available width.

The first of these is called the percentage of parent technique, and can be applied to any container object possessed of a Resize event. You simply include in your code the following container sizing handlers, and connect them to the Resize events of whatever containers you want to exhibit this behaviour. You specify in each child container's Tag property the percentage of the parent container that you want it to occupy. The parent container walks its list of children and sizes each accordingly, setting the unspecified dimension to fully occupy the parent container on that axis. Here's the code:

procedure TYourForm.containerTileHorizontal(Sender: TObject); 
  //produces a row of panels, width controlled by the values 
  //in their tags as a percent of the parent client width
  var
    iIndex, iContainerHeight,iContainerWidth, iHeight,iNextTop:integer;
  begin
  inherited;
  iNextTop := 0;
  with TPanel(Sender) do
    begin
    iContainerHeight := ClientHeight;
    iContainerWidth := ClientWidth;
    for iIndex := 0 to Pred(ControlCount) do
      begin Controls[iIndex].Top := iNextTop;
      Controls[iIndex].Left := 0;
      iHeight := iContainerHeight * Controls[iIndex].Tag div 100;
      Controls[iIndex].Height := iHeight;
      Controls[iIndex].Width := iContainerWidth;
      Inc(iNextTop,iHeight)
      end
    end
  end; 

procedure TYourForm.containerTileVertical(Sender: TObject);
  //produces a column of panels, height controlled by the values
  //in their tags as a percent of the parent client height
  var 
    iIndex, iContainerHeight,iContainerWidth, iWidth,iNextLeft:integer; 
  begin
  inherited;
  iNextLeft := 0;
  with TPanel(Sender) do
    begin
    iContainerHeight := ClientHeight;
    iContainerWidth := ClientWidth;
    for iIndex := 0 to Pred(ControlCount) do
      begin
      Controls[iIndex].Top := 0;
      Controls[iIndex].Left := iNextLeft;
      Controls[iIndex].Height := iContainerHeight;
      iWidth := iContainerWidth * Controls[iIndex].Tag div 100;
      Controls[iIndex].Width := iWidth;
      Inc(iNextLeft,iWidth)
      end
    end
  end;

This is well and good, but now we need to resize the controls in the child panels. Continuing in this spirit of simplicity, we have each container walk its list of children and for each control with a Tag value of 1, set their widths so that they finish 16 pixels from the right-hand border of the container. This is the stretch to margin technique.

procedure TYourForm.containerStretchToMargin(Sender: TObject);
  var
    iIndex,iAvailablePanelWidth,iCalculatedWidth:integer;
  begin
  inherited;
  with TPanel(Sender) do
    begin
    iAvailablePanelWidth := Width - 18;
    for iIndex := 0 to Pred(ControlCount) do
      with Controls[iIndex] do
        if Tag = 1 then
          begin
          iCalculatedWidth := iAvailablePanelWidth - Left;
          if iCalculatedWidth < MINIMUM_EDIT_CONTROL_WIDTH then
            Width := MINIMUM_EDIT_CONTROL_WIDTH
          else
            Width := iCalculatedWidth
          end
    end
  end;

It's not difficult to see a number of useful ways to extend this strategy, particularly if you've had exposure to Java (guess where I got the idea). Without writing custom controls, this is a pretty good effort for not much code.

Going much further, though, is rather messier. You can achieve practically any configuration with just what's presented above, but you have to nest it rather elaborately. There's not much can be done about this, because there's only one tag property so it's possible to control only one dimension per parent.

However, if you don't mind writing custom controls (and I love it) this ceases to be a problem. The logic is less challenging than you might have thought, and you can have all the member variables you want.

Currently I'm writing TLayoutBorder, which is pretty much a clean-room rip off of Java's border layout manager. There are five sections, designated North, South, East, West and Middle. Controls dropped in the North and South border sections are tiled horizontally along the top or bottom respectively, whilst controls dropped in the East and West sections are tiled vertically along the right or left.

Controls dropped in the Middle section are centred vertically and horizontally, which doesn't seem terribly useful until you realise that you're supposed to drop another kind of layout manager in the middle.

Which leads me to the other controls I've been working on - TLayoutGridbag.

Also borrowed from Java, this layout is rather more sophisticated, using structure borrowed from HTML tables - it's tabular, but individual cells can span columns and rows. Row height and column width are specified as percentages of the height and width of the layout manager, or as pixel values.

You might well ask what the point is, of using a layout manager that complicated and then specifying absolute co-ordinates. (Don't point out that people do exactly this all over the web. You'll only annoy me.) The answer is that the remainder of the table remains proportional.

Something else I have in mind for a future layout manager is TController, with various descendents, specifically TControllerGrid. TController is an implementation of the controller concept from the MVC (model/view/controller) model of Smalltalk origin.

There's no particular reason to keep squillions of redundant copies of data in an application. It only makes life harder, because you have to work when to update what, and that can be complicated and time-consuming. The MVC model says the data model is a separate thing from the gimcrack that paints it on the screen. With MVC you an object representing the data, a suite of objects representing the various ways of displaying/editing data, and an object that orchestrates the interactions of the model and the various views.

If there's only one copy of the data, it can't get out of synch, and you don't need code to keep it in synch. This is a Very Good Thing.

Moreover, once you start actively managing - in code - the business of which view operate at any moment on what data, you can take advantage of the fact that at any moment you only need one edit control.

Actually I already did this once - in Visual Basic 3, of all things. I created a control array for each type of control, and dynamically created and positioned controls as required, simulating a dozen dialogs with a single form and half a dozen prototype controls (to create the control arrays).

It wasn't a total success. Everybody else hated the technique because it was completely data driven. This made it very hard to modify the interface without a really abstract mind like mine. That was because VB3 doesn't support the creation of custom controls, so I couldn't implement design time behaviour.

In terms of conservation of windows resources - which was the point of the exercise - it was a raging success, and I have high hopes for the technique in Delphi. If it works out, I'll kill two birds with one stone: ultralightweight resource usage, and in-memory data normalisation. Stay tuned.



Written by: Peter Wone
April '98

Image of Arrow linked to Next Article
Image of Line Break
[HOME] [TABLE OF CONTENTS] [SEARCH]