Smart Mobile currently ships with a rather spartan number of custom controls. Originally we were aiming at a rather minimal RTL and only the basic iOS widgets, this is how C# does it when it comes to iOS development. While we have the rounded iphone menu in the IDE, what is missing is a more versatile menu. Both Android and iOS devices have scrollable list menus that are flat and designed to cover the iphone’s entire horizontal width. You can then move around it with one finger, and when you touch a single item without scrolling – that is considered a “click”.
In this little article I’m going to create this list control to demonstrate how easy it is.
Roll your own
First, let’s start with the baseclasses. We are going to keep it very simple, reducing the list to only two classes: one representing a list item – and another representing the actual list.
Since iPhone lists have the capacity to include sub-controls, we are not going to cheat and wrap UL / LI tags, but use fully functional TW3CustomControls as list items. This will give us the freedom to not only style our list items – but to add buttons and whatever we want to each element.
So let’s start by defining a couple of classes:
type (* Generic ancestor *) TLSTCustomMenuItem = Class(TW3CustomControl); (* Array type *) TLSTMenuItems = Array of TLSTCustomMenuItem; (* Menu item *) TLSTMenuItem = Class(TLSTCustomMenuItem) private FCaption: String; procedure setCaption(Const Value:String); protected procedure InitializeObject; override; public property Caption:String read FCaption write setCaption; end; (* selection event *) TMenuItemSelectedEvent = procedure (Sender:TObject;Const aItem:TLSTCustomMenuItem); (* Our menu *) TLSTMenu = Class(TW3ScrollControl) private FItems: TLSTMenuItems; FLastY: Integer; FSelected: TLSTMenuItem; FOnSelected: TMenuItemSelectedEvent; procedure HandleTouchBegins(Sender:TObject;Info:TW3TouchData); procedure HandleTouchEnds(Sender:TObject;Info:TW3TouchData); protected procedure HandleItemTapped(Const aItem:TLSTMenuItem); procedure InitializeObject; override; procedure FinalizeObject; override; procedure ReSize;override; public property OnItemSelected:TMenuItemSelectedEvent read FOnSelected write FOnSelected; property Items:TLSTMenuItems read FItems; function Add(Const aItem:TLSTCustomMenuItem):TLSTCustomMenuItem;overload; function Add:TLSTMenuItem;overload; end;
The FItems array is basically a list of the child items. This is actually a bit overkill, since TW3Component already support parent/child management – but it’s Friday so I just used an array to keep track of things.
Ok, let’s move on a bit and look at the implementation for our item:
//########################################################################### // TLSTMenuItem //########################################################################### procedure TLSTMenuItem.InitializeObject; begin inherited; Self.Color:=clWhite; Self.Height:=32; end; procedure TLSTMenuItem.setCaption(Const Value:String); begin FCaption:=Value; innerHTML:=Value; end;
This is a fairly simple setup. We give our item a default color of white, and set the height of the listitem to 22 pixels. The setcaption() is actually one you should look out for. As you can see i use the innerHTML property to set the caption. This is fine for this example – but it will kill all child controls (!). For a proper title, create a child TW3Label and position it by overriding the resize() method.
Ok, let’s look at the main scrolling control. The actual scrolling behavior is inherited from TW3ScrollControl which is a minimalistic, touch based scrollbox. Unlike the Delphi scrollbox – the smart version contains a sub-control called “content” which is what is actually moved.
This means that when we populate our listbox, we have to create our list-items with “content” as parent. Ok, let’s have a look:
//########################################################################### // TLSTMenu //########################################################################### procedure TLSTMenu.InitializeObject; begin inherited; FItems.SetLength(0); end; procedure TLSTMenu.FinalizeObject; begin FItems.Clear; inherited; end; procedure TLSTMenu.HandleTouchBegins(Sender:TObject;Info:TW3TouchData); begin (* remember current scroll position *) if (sender<>NIL) and (sender is TLSTMenuItem) then begin FLastY:=Content.Top; FSelected:=TLSTMenuItem(sender); end else begin FLastY:=-1; FSelected:=NIL; end; end; procedure TLSTMenu.HandleTouchEnds(Sender:TObject;Info:TW3TouchData); begin (* No scrolling but touched? Its a tap *) if Content.Top=FLastY then begin if (FSelected<>NIL) then begin if (FSelected=sender) then begin HandleItemTapped(FSelected); FLastY:=-1; FSelected:=NIL; end; end; end; end; procedure TLSTMenu.HandleItemTapped(Const aItem:TLSTMenuItem); begin if assigned(FOnSelected) then FOnSelected(self,aItem); end; function TLSTMenu.Add:TLSTMenuItem; begin result:=TLSTMenuItem(Add(TLSTMenuItem.Create(Content))); result.OnTouchBegin:=HandleTouchBegins; result.OnTouchEnd:=HandleTouchEnds; end; function TLSTMenu.Add(Const aItem:TLSTCustomMenuItem):TLSTCustomMenuItem; begin if aItem<>NIL then begin if FItems.IndexOf(aItem)<0 then begin BeginUpdate; FItems.Add(aItem); EndUpdate; content.LayoutChildren; end; end; result:=aItem; end; procedure TLSTMenu.ReSize; var mItem: TLSTCustomMenuItem; mSize: Integer; x: Integer; dy: Integer; begin inherited; for x:=0 to FItems.Length-1 do begin mItem:=FItems[x]; if mItem.visible then inc(mSize,mItem.height); end; Content.Height:=mSize; dy:=0; for x:=0 to FItems.Length-1 do begin mItem:=FItems[x]; if mItem.visible then begin mItem.SetBounds(0,dy,width,mItem.Height); inc(dy,mItem.Height); end; end; end;
Everything here should be fairly straight forward. We hook the onTouchBegins and onTouchEnd events on all our list items. Why? Because we have to solve a little problem. Since our list is scrollable, that means we have to somehow distinguish between a scroll swipe (up or down) and an actual tap. To achieve this we set down a simple law: If the Y position of the list is in both events, then we consider that a tap (or selection). If you have moved your finger on the other hand, then the tap is ignored.
The final procedure, namely resize, does two things: first, it calculates the total height of all the list items and size the content control accordingly. Then it loops through and positions the child elements.
Styling
Ok, with the behavior out of the way – we just need to add one final piece: namely styling. So double-click on the project CSS file and add the following:
.TLSTMenuItem { -webkit-user-select: auto; background: #FFFFFF; border-bottom: solid 1px #9A9A9A; font-family: "Helvetica Neue", Helvetica, sans-serif; font-size: 17px; color: #888888; }
Let’s give the control a test-drive, so we add the unit to our mainform’s uses clause, create an instance, and add some items:
unit Form1; interface uses w3system, w3graphics, w3ctrls, w3components, w3forms, w3fonts, w3borders, w3application, unit1; type TForm1=class(TW3form) private { Private methods } {$I 'Form1:intf'} FMenu: TLSTMenu; procedure HandleItemSelected(Sender:TObject;Const aItem:TLSTCustomMenuItem); protected { Protected methods } procedure InitializeObject; override; procedure FinalizeObject; override; procedure StyleTagObject; reintroduce; virtual; procedure Resize; override; end; implementation //############################################################################ // TForm1 //############################################################################ procedure TForm1.InitializeObject; var x: Integer; begin inherited; {$I 'Form1:impl'} W3HeaderControl1.Title.Caption:='Form header'; w3HeaderControl1.BackButton.Visible:=False; FMenu:=TLSTMenu.Create(self); FMenu.Color:=clWhite; FMenu.StyleClass:='TPDFMenu'; FMenu.Content.height:=120; FMenu.Content.color:=clGreen; FMenu.OnItemSelected:=HandleItemSelected; FMenu.BeginUpdate; try for x:=1 to 20 do FMenu.Add.Caption:='List item #' + IntToStr(x); finally FMenu.EndUpdate; end; end; procedure TForm1.FinalizeObject; begin FMenu.free; inherited; end; procedure TForm1.HandleItemSelected(Sender:TObject;Const aItem:TLSTCustomMenuItem); begin self.W3HeaderControl1.Title.Caption:=aItem.innerText; end; procedure TForm1.Resize; var dy: Integer; begin dy:=W3HeaderControl1.top + W3HeaderControl1.height + 2; FMenu.SetBounds(0,dy,width,height-dy); inherited; end; procedure TForm1.StyleTagObject; begin //Custom styling end; end.
As you can see from the code, we catch the touch events and change the title of a header control we added to the form. This makes it quite easy to see what you are doing.
You can download the project file here (zip archive) and play with it!
While I agree that it would be great to have as much “native” components as possible, wouldn’t it be helpfull to find a way to use an existing JS/HTML5 GUI Lib (such as Teleriks Kendo)? Do you think that can be done?
PMM
It can be done, but that would defeat the purpose of using pascal. Native JS libraries dont have stuff like inheritance and all the good stuff from pascal, and in many cases they dont even have destructors. While we could have wrapped systems like Sencha – it would perhaps look better, but the underlying system would still be alien and introduce a whole bunch of problems that would be alien to the average pascal programmer. In time, our component model will come into its own – and the benefits will be more visible.
It is (should be) possible to use existing libraries, but we haven’t had enough time to demonstrate this with complete packages.
Many of the solutions we use today (both in the RTL and in various demos) are in fact wrappers for existing browser technology and existing solutions. There are both pros and cons, of course, with this. If you choose to wrap an existing component library, you might miss a lot of the compiler features (obfuscation and smart linking) and you might end up with a framework where the gap between your app-code and your component-package-code feels just a bit to large.
Check out the Box2d wrapper CWBudde wrote a few days ago.
http://smartmobilestudio.com/2012/07/04/box2d-support/
That is one example of such a wrapper.
We made a choice a while a go to create our own basic components, but we have always planned for an open components structure that will make it possible for everyone to write/wrap their own component packages and use these (and even share/sell these to other Smart users). During the autumn we will (hopefully) see lots of such solutions. One of the obstacles today is the visual designer and registration of such components in the IDE. This has a very high priority these days, but the complexity of this work is rather indescribable. During the alpha (and partly the beta) development stage, we have experimented with several solutions to get what we want, but it’s really a brain-twister to get all the pieces right.
Steema Software has probably done what you have requested. They had a html-chart solution already, and created a Smart-version of that. The missing pieces here are 1) make sure that the wrapper fully supports smart linking and obfuscation, and 2) get the component registered in the IDE (i.e. get the component onto the component palette, get the properties into the object inspector and get the live rendering/mock representation into the visual designer.
The component works like a charm if you’re doing it by code and avoid the packing of the generated code (i.e. obfuscation & smart linking).
http://smartmobilestudio.com/2012/04/26/steema-teechart-converted-to-smart/
Almost forgot:
André Mussche also wrapped a DHTMLX grid back in January, during the Alpha testing.
https://plus.google.com/110131086673878874356/posts/8tLVcw7djom
Smart does now support variants as fully fledged JavaScript class containers. Ie instead of using FGrid: TObject, you use FGrid: Variant, and you’ll automatically have full support for all the JavaScript methods available. Thus, no need to wrap up every property/method in the pascal class and do a binding to the equivalent JavaScript member trough a asm-section. You can just call FGrid.setHeader directly from your Pascal source. Now THAT is nice indeed 🙂
Think about the possibility of wrapping Sencha component libraries like in ExtPascal project. They made a scanner that parses the documentation and generates all wrappers automatically for Delphi server side components. The same could be done without much effort for Smart.