tags:

views:

715

answers:

3

Hello

I have some forms in my application which have different "states" depending on what the user is doing; for exemple when listing through his files the form displays some data about that file in a grid, but if he clicks on some button the grid is replaced by a graph relating to it. Simply said, the controls in the form depends on the what the user wants to do.

Of course the obvious way of doing that was showing/hidding controls as needed, works like a charm for small numbers but once you reach 10/15+ controls per state (or more than 3 states really) it's unusable.

I'm experimenting with TFrames right now: I create a frame for every state, I then create an instance of each frame on my form on top of each other and then I only display the one I want using Visible - while having some controls on top of it, out of any frame since they all share them.

Is this the right way to do what I want, or did I miss something along the way ? I thought I could create only one tframe instance and then chose which one to display in it but it doesn't look that way.

Thanks

+16  A: 

Looks like Frames are an excellent choice for this scenario. I'd like to add that you can use a Base Frame and Visual Inheritance to create a common interface.

And to the second part: You design a Frame like a Form but you use it like a Control, very few restrictions. Note that you could just as easily use Create/Free instead of Show/Hide. What is better depends on how resource-heavy they are.

Henk Holterman
+8  A: 

There's a better way to handle dealing with the frames that won't take up nearly as much memory. Dynamically creating the frames can be a very elegant solution. Here's how I've done it in the past.

On the form, add a property and a setter that handles the placement of it on the form:

TMyForm = class(TForm)
private
  FCurrentFrame : TFrame;
  procedure SetCurrentFrame(Value : TFrame);
public
  property CurrentFrame : TFrame read FCurrentFrame write SetCurrentFrame;
end;

procedure TMyForm.SetCurrentFrame(Value : TFrame)
begin
  if Value <> FCurrentFrame then
  begin
    if assigned(FCurrentFrame) then
      FreeAndNil(FCurrentFrame);
    FCurrentFrame := Value;
    if assigned(FCurrentFrame) then
    begin
      FCurrentFrame.Parent := Self;  // Or, say a TPanel or other container!
      FCurrentFrame.Align := alClient;
    end;
  end;
end;

Then, to use it, you simply set the property to a created instance of the frame, for example in the OnCreate event:

MyFrame1.CurrentFrame := TSomeFrame.Create(nil);

If you want to get rid of the frame, simply assign nil to the CurrentFrame property:

MYFrame1.CurrentFrame := nil;

It works extremely well.

Tim Sullivan
+1, but note that the code as it is invites leaking memory. Unless MYFrame1.CurrentFrame is explicitly set to nil before or when MYFrame1 is destroyed it will not be freed. It would be better to pass MyFrame1 as the owner to the constructor, making use of automatic lifetime management in the VCL.
mghie
@mghie: IIRC the Parent of a control also takes over lifetime management, so there should be no memory leak in Tim's code.
Ulrich Gerhardt
Actually, I think if instead of calling TSomeFrame.Create(nil) you instead called TSomeFrame.Create(Self) that would make the form the Owner of the frame, and the form would free it when it is destroyed itself. Alternately, you could set CurrentFrame to nil in the OnDestroy event. Changing from one frame to another will not cause memory leaks, as the setter frees the FCurrentFrame before setting it to another frame.
Tim Sullivan
Now that I've reviewed some of my old code, I personally used interfaces for the frames. As a result, the frame is freed automagically when FCurrentFrame is set to nil. However, this is more advanced than my example above.
Tim Sullivan
@Ulrich: Thanks for the comment, it seems that you are right, as TWinControl.Destroy does indeed call Destroy on all its controls. It kind of replicates what TComponent.DestroyComponents does. I don't know why controls need to be handled differently than other components, do you? Anyway, the code in the answer is not susceptible to memory leaks as long as the old frame is immediately freed when a new one is set as active.
mghie
+2  A: 

I have a word for you : TFrameStack. Simply what the name suggests.

It has a few methods: PushFrame(AFrame), PopFrame, PopToTop(AFrame), PopToTop(Index), and a few Properties: StackTop; Frames[Index: Integer]; Count;

  • Should be self explanatory.

The Frame at StackTop is the visible one. When doing ops like Back/Previous you don't need to know what frame was before the current one :) When creating the Frame you can create and push it in one go FrameStack.Push(TAFrame.Create) etc, which creates it calls the BeforeShow proc and makes it visible, returning its index in the stack :)

But it does rely heavily on Inheriting your frames from a common ancestor. These frames all (in my Case) have procedures: BeforeShow; BeforeFree; BeforeHide; BeforeVisible. These are called by the FrameStack Object during push, pop and top;

From your main form you just need to access FrameStack.Stacktop.whatever. I made my Stack a global :) so it's really easy to access from additional dialogs/windows etc.

Also don't forget to create a Free method override to free all the frames ( if the owner is nil) in the stack when the app is shut down - another advantage you don't need to track them explicitly:)

It took only a small amount of work to create the TFrameStack List object. And in my app work like a dream.

Timbo

Despatcher