Wednesday, May 16, 2012

Laying out Smart applications with Layout Manager [1]

Smart Mobile Studio was released today. To celebrate this – in my opinion – very important step for the Object Pascal language, I have prepared a series of articles on the Layout Manager – a feature of the Smart RTL that I wrote.

Smart is an excellent tool, but in some areas it clearly shows that there was only so much time allowed for the development. I’m sure Lennart will polish the rough edges in next releases but for now we have to do with what we have.

One of those rough edges is component placement. You can drop components on the designer and set their size but there’s no mechanism for dynamic size adjustments. In other words – there is no Align, no Anchors, no Margins and no layouts (as TFlowPanel and TGridPanel in Delphi). We have to resize components in code, in the overloaded Resize method.

I quickly got sick of writing resizing code and decided to alleviate the problem by writing a layout manager. It does not work in the designer, you have to declare and use it in code, but still it is a big simplification over the default “just call SetBounds from Resize” approach. I donated layout manager to the Smart project and it is included in the 1.0 release as the w3layout unit.

This article starts a short series which will explain layout manager on simple examples. All of those examples are also included with the Smart installations (look in Demos, LayoutManagerDemo).

The first example (LayoutDemo1 project) replicates one of first Delphi demos – Edit/Button/Memo. We want to have an edit field on the top of the form, a button underneath it and a memo control which fills all the remaining form space.

We can start by dropping controls on the design form and renaming them appropriately.

image

In Delphi we would simply set Edit1.Align to alTop, Button1.Align to alTop and Memo1.Align to alClient. Well, we can do something similar in Smart.

procedure TForm1.InitializeObject;
begin
  inherited;
  {$I 'Form1:impl'}
  FLayout :=
    Layout{1}.Client(Layout.Margins(3).Spacing(3), [
      Layout{2}.Top(Edit1),
      Layout{3}.Top(Button1),
      Layout{4}.Client(Memo1)
    ]);
end;

This code creates a layout (FLayout is defined a field of type TLayout in the form class). This layout contains of an outer layout 1 (marked {1} in the code) containing three inner layouts (2, 3, 4). Each of the inner layouts contains its own component.

Layout Basic

Before I continue I shall say something about layouts so that you’ll understand better what’s going on.

Each layout lies in a container. In the example above, layout 1 doesn’t have container defined (we’ll fix this in the Resize) while layouts 2 to 4 are contained in the layout 1.

Each layout can contain one or more components, where by a component I mean another layout or a Smart control.

Each layout can have configuration options defined. Configuration options are passed as an optional first parameter. In the example below, layout 1 has margins set to 3 and spacing to 3.

Margins specify the distance between the layout and the container which holds this layout. You can set all margins to the same number or specify different value for each margin (top, bottom, left, right).

Padding (not used in this example) is similar to margins but works inside. Padding specifies the distance between the layout and contained controls.

Spacing only applies to a layout that contains more than one control and specifies additional spacing between controls.

Layout can be Top, Bottom, Left, Right, or Client aligned.

Some placement parameters (position, size) can be determined from the external container (the one I said we’ll specify in the Resize). Others are taken from the contained control. In the example above, Layout 2 height will be set from the Edit1 height. Same goes for the Layout 3 height. Edit1 and Button1 width will be, however, determined from the layout width. It sounds complicated but really isn’t. Think about what happens in Delphi when you set the Align property. Some placement parameters are set automatically and other are left alone. [For the alTop layout, for example, Left, Top, and Width are set while Height isn’t.] The same placement parameters are set automatically in Smart while other (Height in the example of a Top alignment) are determined from the nested controls.

You can set layout width and height manually by providing a first parameter of Layout.Width or Layout.Height.

Resizing layouts

The code above only defined a layout, it didn’t resize it. To do that, we have to add one line to the overloaded Resize method.

procedure TForm1.Resize;
begin
  inherited;  
  FLayout.Resize(Self);
end;

With that we are telling the layout manager that the outside container is the form itself (Self) and that the layout should please place itself into this form.

Example, revisited

I tried to document the layout we’re talking about in a graphical form. Maybe this will make the concepts easier to understand.

image

Layout 1 is placed on a form. Distance between the Layout 1 and form borders is 3 pixels (set by the Margins parameter).

Layout 2 is placed at the top of the Layout 1. There’s actually no difference between Layout1.Top and Layout2.Top (same goes for .Left and .Width) – I just had to draw a little space between to separate the two visually. Edit2 has exactly the same .Width, .Height, .Left and .Top as Layout 2.

Then there’s a Spacing (3) pixels empty space and Layout 3 / Button1 are laid out just like Layout 2 and Edit1.

Layout 3 is followed by another Spacing pixels of empty space and then Layout 4 takes all the space there was left. Memo1 is resized to take all the space inside Layout 3.

Result

Viewed in Chrome, the form looks quite nice (as does in other browsers and on mobile devices).

image

18 comments:

  1. I don't see any of the images. They are on 17slon.com and it might be down...

    ReplyDelete
    Replies
    1. Seems to be working fine at the moment. I tried from two different locations and both can load images.

      Delete
  2. @François Or your firewall is just refusing this web site. Because I can see them as expected.
    @Gabr Nice article!
    It is great having such a layout manager. The fluent interface is a bit strange at first (why not call the records "NewLayout" or "Contains" or "Align" instead of Layout)? It takes a while, or to read the code/blog article to find out what does the syntax mean.
    But when you get the principles, it is very easy and powerful. Very well done!
    When I evaluated the beta, working with components resizing was a real PITA. With your unit, this is very easy.
    And the generated JavaScript code is incredible: the fluent interface is shorter in code size than a property-driven writing.
    Nice!

    ReplyDelete
    Replies
    1. Well, it's shorter because there's a big RTL hiding behind :)

      Delete
  3. Observation and Question:

    How does Layout.Margins not result in Layout{2} with all subsequent Layout{N}'s being Layout{N+1} ?

    Assuming that it doesn't, this is one of the problems I have with "fluent" style API's - they often have such inconsistencies and contradictions for the sake of some notion of convenience that expresses the preferences of the original author rather than any coherent design that makes sense to any one else seeing it (and using it) for the first time.

    And even if it does, it still doesn't really make much sense as whatever Layout{2}.Margins is, it doesn't appear to be a Layout with much in common with the other Layout{N}'s.

    ReplyDelete
    Replies
    1. I'm only guessing at what you're trying to say so my answer may be off ...

      Layout is just a factory class. You can think of it as of a namespace. Layout.Top, Layout.Left ... generate a new layout while Layout.Margins, Layout.Spacing ... generate new configuration settings.

      Yes, it may be hard to learn, but it is short and intuitive once you learn it.

      Delete
  4. If you have to learn it then - by definition - it's not intuitive. Merely convenient and reflective of an internal thought process, not a coherent "design". And that's my point.

    Layout.Top << Generates a new layout
    Layout.Margins << doesn't apparently
    Layout.Spacing << doesn't apparently
    Layout.Client << Generates a new layout

    There is nothing to indicate which methods are factories and which do something else. The very fact that it has to be explained is simple proof that it isn't intuitive at all.

    More intuitive would be for layout's to have margin and spacing properties (this seems to me to be important for more sophisticated layout arrangements anyway), making these simply properties of layouts, and leaving the Layout factory class to be just, and only, that :

    layout := Layouts.ClientLayout;
    layout.SetMargins(3);
    layout.SetSpacing(3);

    layout.Add([Layouts.TopLayout,
    Layouts.TopLayout,
    Layouts.ClientLayout]);

    Far more intuitive imho. This would not need explanation to clarify which usage patterns of the factory class yield layouts and which usages do not. Use the factory class to obtain layout objects, set properties of the layout objects on the layout objects themselves.

    Simple and _obvious_ == Intuitive

    If you absolutely HAD to, you could still use the "fluent style" (I prefer to call it "lazy style") if you really can't be bothered to write clearly understandable code that will be read and have to be understood on numerous occasions and insist on saving a few seconds when creating the code on that one-off occasion when it is created:

    fLayout := Layouts.ClientLayout.SetMargins(3).SetSpacing(3).Add([Layouts.TopLayout, Layouts.TopLayout, Layouts.ClientLayout]);

    ReplyDelete
    Replies
    1. We'll just agree to disagree then. I always tend to write as little code as possible to do the work.

      Delete
  5. This layout system has a lot in common with the de-facto standard C# layout library (coded by icaza himself) monotouch.dialog (https://github.com/migueldeicaza/MonoTouch.Dialog). It also follows the same layout as MUI which is used on both linux but has roots all the way back to the amiga.

    It sort of goes without saying that intuition (which is a snap-to function of the brain) wont always work when you are facing a completely new paradigm.

    ReplyDelete
  6. @Gabr - Nice! Thanx for creating the w3layout unit, and for creating the tutorials.

    ReplyDelete
  7. Hi Gabr,

    How would i use Layout Manager to created a Toolbar aligned top. and 5 buttons aligned left on toolbar?

    Thanks
    Shane

    ReplyDelete
  8. Simple.

    FLayout := Layout.Top(
    Layout.Left([W3Button1, W3Button2, W3Button3, W3Button4, W3Button5])
    );

    ReplyDelete
  9. Sorry, I was refering to a TW3Toolbar, and TW3ToolbrButton. I will try it with a plain of Panel and button though as well.

    ReplyDelete
    Replies
    1. Given toolbar W3Toolbar1 and Toolbuttons Btn1, Btn2, Btn3 you can do:

      FLayoutT := Layout.Top(W3Toolbar1);
      FLayoutB := Layout.Left(Layout.Padding(2), [Btn1, Btn2, Btn3]);

      FLayoutT.Resize(Self);
      FLayoutB.Resize(W3Toolbar1);

      Delete
    2. the buttons are actually ToolbarButtons and they are created with the Toolbar's "Add" function. There is already a buttonspace property. So, having said that, i went back to the Panel and Button controls to try out your suggestions.

      Procedure TForm1.InitializeObject;
      Begin
      inherited;
      {$I 'Form1:impl'}
      fbuttonPanel:= TW3panel.Create(self);
      fButtonPanel.Height:= 40;
      fbutton1:= TW3Button.Create(fbuttonPanel);
      fbutton2:= TW3Button.Create(fbuttonPanel);
      fbutton3:= TW3Button.Create(fbuttonPanel);

      fLayoutP:= Layout.Top(fButtonPanel);
      fLayoutB:= Layout.Left(Layout.Padding(2), [fButton1, fButton2, fButton3]);

      End;

      Procedure TForm1.Resize;
      Begin
      inherited;
      fLayoutP.Resize(Self);
      fLayoutP.Resize(fbuttonPanel);
      End;


      Although the buttons are left aligned, they seem to stack on top of one another

      I doubt i will ever get the hang of using this layout class. Its a great idea, and i gave it a really good go, but it is way to complicated for me. I think i will have to stick with the manual manipulation of controls. Ever thought about writing a book on the "Layout" class? Just Kidden

      Shane

      Delete
    3. You did everything right, but there's a typo in TForm1.Resize. Second .Resize call should read

      fLayoutB.Resize(fbuttonPanel);

      You are resizing fLayoutP twice ...

      Delete
  10. LOL! - I sure am - thanks! Somethime you can look forever and never see the obvious!

    Shane

    ReplyDelete
  11. gabr, i also added this as an alternative example at the end of my post

    http://smsbasicsandbeyond.blogspot.com/2012/06/button-bars-part-i.html

    Im sure i will attempt to use it some more in some of my future posts as well...so stand by

    Shane

    ReplyDelete