Ramblings from a software developer

I have found Elevate Software’s WebBuilder product to be a very productive environment for me to use to develop web based user interfaces. As a long term Delphi developer, it suits me very well, allowing me to take all the language skills in Delphi, and create very capable Javascript single-page applications. It contains a core set of UI components which at first may seem to be lacking advanced options, but like Lego, you can make very nice complex components with the raw tools provide. I have implemented both an advanced grid, and a slider control using the provided TPanels, TLabels and other components.

The finished result:

The finished WebBuilder grid

Okay, so to start off I design the UI with a single TPanel (called in this case pnlProductMaster) that is in the right location (on another TPanel which is the grid parent). Onto the pnlProductMaster panel I put the other elements that I want. This could be a set of more panels to make cells, or anything you want. In the example case, I have an image, a title label, some explanation text and a button. Make the template panel invisible so that it won’t appear to the user ever.

The template for the grid

Now, the implementation to make this template into a grid of this is perhaps currently a little crude. I have considered making things more generic, but then I’d lose control over the fine details, and I like to put a lot of polish into my UI. Thus instead of making a generic framework that allows me to access Row[5].Label[2].Caption, I rather have a Row[5].TitleLabel.Caption. First, I define a class to hold the information about my panel components:

TProductPanel = class
private
 m_xProductPanel: TPanel;
 m_xTitleLabel : TLabel;
 m_xHeaderLabel : TLabel;
 m_xLogo : TImage;
 m_xSelect : TButton;
 m_bSelected : Boolean;
 m_szProductJSON : String;
 m_xOptionsList : TStringList;
 
 procedure SetSelected(bValue : Boolean);
public 
 constructor Create(xMainPanel : TPanel);
 destructor Destroy; override;
procedure SetOptions(xOptionsList : TStringList);
 
 property MainPanel: TPanel read m_xProductPanel write m_xProductPanel;
 property TitleLabel : TLabel read m_xTitleLabel write m_xTitleLabel;
 property HeaderLabel : TLabel read m_xHeaderLabel write m_xHeaderLabel;
 property Logo : TImage read m_xLogo write m_xLogo;
 property Select : TButton read m_xSelect write m_xSelect;
 property ProductJSON : String read m_szProductJSON write m_szProductJSON;
 property OptionsList : TStringList read m_xOptionsList;
 property Selected : Boolean read m_bSelected write SetSelected;

end;

Yes, sorry, I use a personal version of “hungarian notation” and “m_” means a member variable, and “x” means an object. You can make it to fit your style.

Then I have a procedure to create a panel to represent an item in the data. The parameters are I hope obvious, one specifying the index, and the other passing in data to be stored against the row. This helps later when the user selects it, as the data is ready and waiting.

procedure TfrmShop.CreatePanelFor(nProductLoop : Integer; xOptionsList : TStringList);
var
 xPanel : TProductPanel; 
begin
 xPanel := TProductPanel.Create(DuplicatePanel(pnlProductMaster, pnlProductList));
 xPanel.MainPanel.Tag := nProductLoop;
 xPanel.TitleLabel := DuplicateLabel(lblProductName, xPanel.MainPanel);
 xPanel.HeaderLabel := DuplicateLabel(lblProductHeader, xPanel.MainPanel);
 xPanel.Logo := DuplicateImage(imgProductLogo, xPanel.MainPanel);
 xPanel.Select := DuplicateButton(btnProductSelect, xPanel.MainPanel);
 
 xPanel.Select.Tag := nProductLoop;
 xPanel.HeaderLabel.Tag := nProductLoop;
 xPanel.Logo.Tag := nProductLoop;
 xPanel.TitleLabel.Tag := nProductLoop;

 xPanel.MainPanel.Top := (nProductLoop * (xPanel.MainPanel.Height - 1)) - 1;
 
 xPanel.SetOptions(xOptionsList);

 m_xProductDisplay.Add(xPanel);
end;

The panel object is stored in a TObjectList called m_xProductDisplay. The DuplicatePanel, DuplicateLabel etc are pretty similar, and are generic functions to replicate a specific panel. In this way, a copy of the template panel is built up.

function DuplicatePanel(xReferencePanel : TPanel; xParentPanel : TPanel) : TPanel;
begin
 Result := TPanel.Create(xParentPanel); // xReferencePanel);
 Result.Name := 'AutoPanel' + IntToStr(g_nAppendName);
 Inc(g_nAppendName);
 CopyPanelDetails(Result, xReferencePanel);
end;

procedure CopyPanelDetails(xTargetPanel : TPanel; xReferencePanel : TPanel);
begin
 xTargetPanel.ShowBorder := xReferencePanel.ShowBorder;
 xTargetPanel.ShowCaption := xReferencePanel.ShowCaption;
 xTargetPanel.ShowCloseButton := xReferencePanel.ShowCloseButton;
 xTargetPanel.ShowShadow := xReferencePanel.ShowShadow;
 xTargetPanel.Hint := xReferencePanel.Hint;

 xTargetPanel.Width := xReferencePanel.Width;
 xTargetPanel.Height := xReferencePanel.Height;
 xTargetPanel.Color := xReferencePanel.Color;
 xTargetPanel.Left := xReferencePanel.Left;
 xTargetPanel.Top := xReferencePanel.Top;

 xTargetPanel.OnClick := xReferencePanel.OnClick;
 xTargetPanel.OnMouseEnter := xReferencePanel.OnMouseEnter;
 xTargetPanel.OnMouseLeave := xReferencePanel.OnMouseLeave;
 xTargetPanel.OnMouseMove := xReferencePanel.OnMouseMove;
 xTargetPanel.OnMouseDown := xReferencePanel.OnMouseDown;
 xTargetPanel.OnMouseUp := xReferencePanel.OnMouseUp;
end;

The important part here is that the important properties of the panel, or other component, are all copied. For the panel, I don’t copy the captions as they are likely to not be duplicated. For labels, I have a parameter to determine if the label caption should be duplicated too. I have at points sat trying to work out why my new code isn’t working, and of course I’d not copied the appropriate property. The mouse events in particular which are used to highlight the panels as the user mouses over. Note that the tag of the items was set in the creation of the panel, so all items on the panel know which row they are representing, and all of them have the MouseEnter and MouseLeave set.

procedure TfrmShop.pnlProductMasterMouseEnter(Sender: TObject);
begin
 TProductPanel(m_xProductDisplay[TComponent(Sender).Tag]).Selected := True;
end;

procedure TProductPanel.SetSelected(bValue : Boolean);
begin
 m_bSelected := bValue;
 if m_bSelected then
 begin
 MainPanel.Color := clMintCream;
 end
 else
 begin
 MainPanel.Color := clWhite;
 end;
end;

In some applications, I’ve not only changed the background colour, but also shown and enabled things like buttons or links that allow activity on the grid item. Not having it visible for all items stops the UI being overwhelmed with a repeat that isn’t useful unless you are over the item.

A more traditional grid with mouseover TLink

 

So, now you just have to fill the grid with data.

procedure TfrmShop.GetProductInfoComplete(nResult : Integer; szProductJSON: String);
var
 nLoop : Integer;
 xList : TStringList;
begin 
 try
 lblPreparing.Visible := false;
 
 m_xProductDisplay.Clear;
 
 nLoop := 0;
 xList := TStringList.Create;
 try
 json_ReadStringListPaired(szProductJSON, 'p' + IntToStr(nLoop), xList);
 while xList.Count > 0 do
 begin
 CreatePanelFor(nLoop, xList);
 
 Inc(nLoop);
 json_ReadStringListPaired(szProductJSON, 'p' + IntToStr(nLoop), xList);
 end;
 pnlSignIn.Top := ((nLoop) * (pnlProductMaster.Height - 1)) - 1;
 pnlSignIn.Visible := not frmWelcome.LoginActive;

 
 finally
 xList.Free;
 end; 

 except
 on errInfo : EError do
 begin
 Report('GetProductInfoComplete Error ' + errInfo.Message);
 end;
 end;
end;

I hope that this gives some explanation of how it is possible to make nice looking and interactive grids in WebBuilder.

Please note that I have no link to Elevate Software other than being a happy user of their products. This is provided to the world unsolicited, and as-is. If you wish to discuss WebBuilder, I hang out on the relevant newsgroups.