Blog
All Blog Posts | Next Post | Previous PostInitiateAction - a small hidden gem to build Graphic User Interfaces
Friday, January 8, 2010
Recently, I have talked with a couple of Delphi programmers about InitiateAction method and I was surprised that they didn't know about it and found it very interesting to use it to help building end-user interfaces, so I thought it would be interesting to comment about it here, too.Here is the problem: years ago I was building a small component and I wanted it to display itself in a specified state, depending on external variables and properties. To use a recent example: The TIDEPaletteButtons component. It is a visual component just like the Delphi palette toolbar, which displays all the available components and controls that the end-user can put in a form that is being designed. When the end-user clicks a button item in this palette, the button stays pressed down, indicating that a component is selected, meaning that if the end-user clicks the design form, that component will be inserted. Once the component is inserted, the button goes up again indicating that no component is selected anymore.
Now the problem begins. All the component placing operation is performed by the form designer component, not the TIDEPaletteButtons. For example, end-user can press "Esc" key to cancel placing. The "Esc" key is detected and handled by the form designer. If this happens, then the TIDEPaletteButtons should raise up the button associated with the component that was being inserted. Another example: all the component placing operation can be done programatically from the form designer. For example, using the following code:
IDEEngine1.Designer.PlaceComponentClass(TEdit);
the designer enters in insert mode, so if the user clicks or drag the mouse in the form, a TEdit component will be inserted. In this mode, the TIDEPaletteButtons component should display the button item associated with the TEdit in a "pressed" state.
How to solve this? Should we care about all places where the form designing state can change? What if we forget a place and the interface becomes inconsistent at some point? Well, this is a problem related to "action" concept: in the end, the TIDEPaletteButtons should just display itself according to the state of the form designer, and according to the component being inserted.
The InitiateAction helps us to do that. The InitiateAction is called by the VCL just like the TAction.OnUpdate events are called to update the action components. We can override it and use it as if it was the "OnUpdate" event of our component. It is useful because we don't need to care about when to update our component, we just need to write a code that udpates the component according to a state. To ilustrate, here is the code used in TIDEPaletteButtons:
type TIDEPaletteButtons = class(TCategoryButtons) ... public procedure InitiateAction; override; ... end; procedure TIDEPaletteButtons.InitiateAction; var c: integer; d: integer; ItemToSelect: TButtonItem; begin ItemToSelect := nil; if (FEngine <> nil) and (FEngine.Designer <> nil) then for c := 0 to Categories.Count - 1 do for d := 0 to Categories[c].Items.Count - 1 do if TIDEPaletteButtonItem(Categories[c].Items[d]).FClass = FEngine.Designer.PlacedComponentClass then begin ItemToSelect := Categories[c].Items[d]; break; end; SelectedItem := ItemToSelect; end;
All it does is to select the button item that corresponds to the class being inserted in the designer. If no class is being inserted, then no item is selected, and all buttons are raised up. I don't care about when this changes, I just care about what the state it is.
You can use InitiateAction not only for building custom components, but in your own application. You can override the InitiateAction method of your form, and perform the updates there. For example, we could use it to update a label that indicates what is the name of the currently selected control:
type TForm1 = class(TForm) Label1: TLabel; ... public procedure InitiateAction; override; ... end; procedure TForm1.InitiateAction; begin if Self.ActiveControl <> nil then Label1.Caption := Self.ActiveControl.Name else Label1.Caption := '(none)'; end;
This is a simple and easy to mantain code, instead of using the traditional approach of finding the places in the code which are executed when the focus changes, and updating the label from there. Personally I think this is clean and less subject to visual bugs.
The only thing you must be aware of is that this method is frequently called - whenever the application is idle. So your code must be very light to not use too much CPU, and avoid flickering. The example above is ok because even when we are setting Caption property all the time, TLabel only effectively updates the caption of windows label control when it really changes. Otherwise we should check it in our code, to avoid flickering the label and unnecessary CPU usage.
Wagner Landgraf
This blog post has not received any comments yet.
All Blog Posts | Next Post | Previous Post