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
 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);
 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;


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);
 xPanel : TProductPanel; 
 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;


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;
 Result := TPanel.Create(xParentPanel); // xReferencePanel);
 Result.Name := 'AutoPanel' + IntToStr(g_nAppendName);
 CopyPanelDetails(Result, xReferencePanel);

procedure CopyPanelDetails(xTargetPanel : TPanel; xReferencePanel : TPanel);
 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;

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);
 TProductPanel(m_xProductDisplay[TComponent(Sender).Tag]).Selected := True;

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

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);
 nLoop : Integer;
 xList : TStringList;
 lblPreparing.Visible := false;
 nLoop := 0;
 xList := TStringList.Create;
 json_ReadStringListPaired(szProductJSON, 'p' + IntToStr(nLoop), xList);
 while xList.Count > 0 do
 CreatePanelFor(nLoop, xList);
 json_ReadStringListPaired(szProductJSON, 'p' + IntToStr(nLoop), xList);
 pnlSignIn.Top := ((nLoop) * (pnlProductMaster.Height - 1)) - 1;
 pnlSignIn.Visible := not frmWelcome.LoginActive;


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

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.