Documente Academic
Documente Profesional
Documente Cultură
2nd Edition
By Erick Engelke
erickengelke@gmail.com
Kitchener, Canada
Copyright © 2016 Erick Engelke
All rights reserved. No parts of this book may be reproduced, stored in a retrieval system
or transmitted in any form or by any means without prior written permission from the
author, except in brief quotations embedded in critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy of the
information presented. However, the information in this book is sold without warranty,
either express or implied. Neither the author, the publisher or its affiliates, dealers and
distributors will be held liable for any damages caused or alleged to be caused directly or
indirectly by this book.
The author has endeavored to provide trademark information about all the companies and
products mentioned in this book by the use of capitals. However, the author and publisher
cannot guaranty the accuracy of this information.
I am very grateful for the keen proofreading and eternal patience of Tim Young of Elevate.
In 1995 there was a new product called Delphi which revolutionized Windows
development for many programmers. It introduced the Object Pascal language and the
Visual Component Library (VCL) which greatly simplified writing Windows applications,
and database applications in particular.
The idea was called RAD for Rapid Application Development and it benefitted small
shops and individual programmers because they could quickly produce applications which
competed with big firms, and it helped big firms because they could do more… faster.
In the years since, Delphi has been reworked to support native Linux, Android, IOS and
Macintosh applications. Its open source cousin: Free Pascal with Lazarus have supported
these and other platforms too. Their motto is ‘write once, compile anywhere’.
Several groups have translated Delphi’s basic architecture to Web applications which
generate JavaScript as the run time language. The two I have used are Smart Mobile Studio
SMS and Elevate Web Builder or EWB. They are not identical to Delphi, but still so
similar one feels eerily at home..
This book focuses on EWB. I think it marks an epoch in Web development that ushers in an
age of reasonable tools for fast and reliable production of Web pages.
If you have a Delphi background, you will be instantly familiar with much of EWB. But
even newcomers will find it inviting. By basing the product on the ideas of Delphi, the
Elevate developers have inherited a lot of well-tuned principles.
Pascal is a wonderful language. It has the power of C and thus has been used to write
whole operating systems, but unlike C and its derivatives (C++, JavaScript, PHP), it has
very strong type checking. I believe the loose typechecking of these other languages leads
to many bugs and run-time errors because the execution does not match the programmer’s
intent.
With Pascal, the compiler knows exactly how you have defined a variable or constant or
object, and it uses it only in that manner. This not only makes your execution more
predictable, it means most programming errors are found at compile time and not when
your users go to execute the code. It also means the compiler can use efficient strategies,
and that leads to fast execution.
There is an excellent reference to the general Object Pascal language written by Marco
Cantu. It makes a great read and a reference text, and it is applicable to EWB, Free Pascal,
Delphi and others.
EWB is a product of Elevate Software in the state of New York, USA. They started out in
1998 with database technologies which they sell world-wide. In 2011 recently they
branched into web development with EWB.
I found they have excellent customer service with product support being provided by the
actual developers as well as many satisfied customers.
They test their products rigorously using automated tests, so you are not likely to discover a
bug.
EWB is written in Delphi and bears a strong resemblance to the Delphi Integrated
Development Environment.
There is also a command line version of the compiler for those who chose not to use the
IDE.
When you are ready to ship your completed application, you can chose to compress the
source code. This converts it to minimalist JavaScript functions, variable names and
spacing to compress the size of the output files, and effectively obfuscates it from easy
reading or modification.
For a typical project, compressed and obfuscated HTML and JS files typically hover
around 1 MB total, which is the size of a typical Jpeg file and can easily be loaded by
phones and other network devices on cellular, Wifi and other Internet options.
I will focus on the most popular applications, which are Form based applications. You can
create other pages without using the libraries, but I feel that is best done as an advanced
project, if at all. The libraries benefit you with the wisdom and experience of several years
of development and refinement. And the cost (in size and performance) is low.
The reader is assumed to know some programming language like Pascal, C, PHP, C++,
JavaScript or similar.
This is not a definitive introduction. There are many texts which spend hundreds or
thousands of pages doing justice to that topic. Instead, this is intended to get a typical
programmer up-to-speed in EWB’s implementation of Object Pascal.
Pascal is a case-insensitive language. You can use any combination of capitalization at any
time, so: myObject is the same as MyObject. Just try to follow some convention or your
code will look ugly.
EWB uses Unicode for all elements. It is capable of creating applications for any Unicode
language. However, EWB cannot support some Eastern languages in the Code Editor.
For comments, EWB supports C++like // single line comments and { } for multiline
comments.
Delphi allows dynamic and static range arrays. EWB only supports dynamic arrays. They
are always zero based, and the size (length) of the array must be set in code, just like
dynamic arrays in Delphi.
If you do not come from an OOP backgroup, objects are like C structures but on steroids.
Not only do they contain data, they do so much more. They contain methods, which are
functions and procedures that implement functionality for the object.
Objects offer:
Encapsulation: The object is self-aware. It can contain data that only the objects methods
can access, and it can present that data in special ways. For example, it can act like an
array, but when you access an element, one of the methods is called to perform an action
such as update a cell in an on-screen table. So the upstream program is simplified because
it just appears to access a read-only, read/write or write-only attribute, but in reality it is
calling code.
Inheritance
Classes descend from other classes and inherit their strengths, while adding or replacing
functionality. This creates a class hierarchy with no limit on its depth.
EWB only supports single inheritance, meaning classes have a single parent, which can
also be a descendants of still other classes. This differs from some languages which permit
classes to inherit from one or more direct classes.
Much has been written about the argument of whether single inheritance is better or worse.
I’ll simple leave it as a fact that many great programs have been written with single-
inheritance languages and that it is simpler to understand and implement.
If no inheritance is specified, the class will inherit from TObject. Classes can have four
types of members:
The protected section of a form is not used for anything in the IDE - the IDE manipulates a
special section that is internally called the designer public section. You'll notice that the
IDE puts things in the section at the top with no label - that's the designer public section. It
has the same scope behavior as public.
Private will be for variables and methods you create which only your class can use. By
default, put all variables and methods into private.
Public is the designation for all variables and methods you wish to expose to other classes
and procedures. Move variables and methods to Public when you realize they need to be
accessed externally.
Object methods can be simple functions and procedures, they can also be declared as
virtual, abstract and override.
Virtual methods can be overridden in descendant classes.
Abstract methods indicate the virtual method is NOT implemented in the base class, but
must be implemented by descendant classes. Your application will not compile until you
implement the functionality – which in turn reduces the problem of not implementing all the
requirements.
Inherited methods can augment the functionality in a base class, and can even call the base
class function.
Overloaded methods have multiple declarations. Such as add( x, y : integer) :
integer;
add( x, y : double ):double; overload; Note that EWB, unlike several
other Object Pascal variants, does not require you to use the overload keyword.
Constructors and Destructors are special methods that handle the process of creating and
destroying instances. In Delphi the constructors and destructors allocate and free memory –
which is not necessary in our JavaScript environment. However, they are also useful for
initializing variables and cleaning up afterwards.
Typically, you will not create files like C’s Header files which are separate from the
source code (.H files in the C example). Instead, a declaration section at the top of the
Pascal file does a similar thing.
unit Main;
interface
// types, variables, objects and methods used
// and implemented in this file
implementation
// the rest of the file is private to this file
end.
Pascal is a single-pass language meaning the compiler only reads through the source once,
whereas C is typically a two to four pass language. As a result, Pascal compiles much
faster, but it must be organized more carefully.
If you define, say, TClassDog, and it descends from TClassMammal, then you must define
TClassMammal before you define TClassDOG.
Functions and procedures may be passed variables, but these variables only have scope
inside the method called.
These methods can also reference variables and methods specified in the current class.
Pascal has the love-it-or-hate-it WITH keyword, where you can specify: WITH
InstanceDog do
….
so you do not have to specify the variable name on each line. There are many arguments for
and against this keyword. I’ll leave it to you, but remind you that WITH overrides the
normal scope of any local or class
variables/properties/methods.
Most modern programming languages let you cast from one variable type to another.
We will use an example later where we iterate through visual components on the screen
and only react to TButtons by using is to query the class type. is does not simply look at the
current class type, it works for all inherited types too.
var
i : integer;
c : TComponent;
but : TBbutton;
begin
for i := 0 to Component.Count -1 do begin c = Components[i];
if ( c is TButton ) then begin
but := TButton( c );
but.Caption := ‘a button’; end;
end;
Good programming deals with exceptions you never expected, and those that you did
anticipate too.
In Object Pascal, the keywords are try, except, on exception, and raise.
For example, if the user tries to enter negative money, you should raise an exception.
If ( a < 0 ) then
Raise Exception.Create(‘do not use negative money’);
The component library and the run-time raise exceptions whenever necessary. Use
try..except to handle exceptions.
try a := StrToInt( edit1.Text );
except
ShowMessage(‘Could not convert ‘+edit1.Text+‘ to a number’);
end;
Or you can access the exception class variable to print out the general exception
information.
try
a := StrToInt( edit1.Text );
except
on E: Exception do
ShowMessage(‘Error:’+E.message); end;
In the exception handler you can re raise the exception for the next stacked exception
handler.
Object Pascal also implements a finalize section to implement code that always executes
whether an exception is raised or not.
try
…
finally
.// this code is always called
end;
Visual applications have a default handler if you do not handle the exception. It normally
prints a generic message with the exception text. You can define a TApplication.OnError
event handler which can do other things, such as give your phone support number.
EWB is not an island to itself. You may also wish to call external JavaScript libraries or
access variables in the Document Object Model (DOM), etc. Remember that JavaScript is
case sensitive, so you must be careful with casing of references. However, only the
developer of external interfaces must be concerned with case sensitivity of JavaScript, so
application code which references it from EWB Pascal is free to use any capitalization you
choose. EWB will emit the JavaScript using the case of the external interface definition
and not the case of the references to it.
The most common example of external interfaces is to access the Window’s URL. Here we
see how to switch to Google’s home page.
uses webdom;
s := ‘http://google.com’; window.location.replace( s );
Like Delphi, EWB uses a message queue to sequence operations. Async is used to schedule
future code execution. We have a whole chapter on this later.
One of the most used object classes is TStrings and its relative TStringList which both
encapsulate lists of strings.
After you create() a TStringList, you can add strings with member functions add(), delete().
You can access individual lines as though it were an array of strings. And you can deal
with the entire list as though it were a single string using the .Text attribute; You can sort()
and find() strings.
If you assign strings with a NameValueSeparator (defaults to =), you can use it as though it
were an associative array.
Eg.
var
sl : tstringlist;
name : string;
age : integer;
begin
sl := tstringlist.Create;
sl.add(‘Name=Erick’);
sl.add(‘Password=Friday’); sl.Values[ ‘Age’] := ‘49’;
name := sl.Values[‘Name’]
age := IntToStr( sl.Values[‘Age’]);
end;
The visual form designer is comprised of the form, the tabbed units editor, and the
components one can select.
The object inspector show properties and events related to a selected component. You can
select something visual, like a button, something nonvisual, or even the form itself to adjust
its properties and events.
A messages window at the bottom shows messages such as the compiled size, or errors if a
compiler error is being reported to you.
If you select Project | Options | Application, you can set the title where appears on the
browser’s header. This step is necessary to have a professional looking page.
So we have a blank form. As a matter of practice, save the files right away and run the
form by pressing F9 or Run | Run. It will wish to save the form and the project, put them in
a new subdirectory to keep things organized.
EWB will then show the empty form. Not very interesting yet, just a blank form. Press the
close X button above the blank page, this will return to edit mode.
From the Visual Objects collection on the upper right hand of the screen, pick the T icon
and then tap on the empty form to place it anywhere on the form. This is a text label, it
displays text. Being the first TLabel on your form, it will be called Label1. TEdit is the
class name, Label1 is the instance name.
Then move your mouse over the other icons until you find TButton, which is a button Place
it on the form too: Button1.
Double click on the button you just placed. This will open the code editor on the button’s
OnClick event handler, the method called when one clicks on a button.
This is a program. Press F9 to Run it. Both the label and button are visible, but they say:
Caption, which is the default text for the Caption property. But when you press the button, it
changes the Label1’s Caption property to ‘hello’, and through encapsulation, that updates
the screen automatically.
begin
i := Edit1.Text;
Label1.Caption := i + 1;
end;
This will not compile when you press F9, because i is an Integer type and Edit1.Text
(which is the user input) is a string and not all strings can be turned into numbers. And it
would also complain because integers cannot be converted to strings in the next line
begin
i := StrToInt(Edit1.Text);
Label1.Caption := IntToStr(i+1);
Now it compiles and runs. Enter a number and we will show the following integer.
But you could still enter a text string like George instead of a number, and you will get an
error message: Invalid Integer Value and a line number like 619.
Let’s see the actual code. Open the output subdirectory from your source subdirectory and
you will find both an HTML and a JS (JavaScript) file. The error refers to the StrToInt()
function which is implemented not in Pascal but in JavaScript.
function strtoint($0)
{
if ($0 != "" && $0[0] == "0" && ($0[1] == "x" || $0[1] ==
"X"))
var $1 = parseInt($0,16); else
var $1 = parseInt($0,10); if (!isNaN($1))
return $1;
else
throw "Invalid integer value";
};
Yes, your code is converted to JavaScript and executed by the JavaScript engine in your
web browser.
A nice way to deal with this is using Try/Except exception blocks.
procedure TForm1.Button1Click(Sender: TObject); var
i : integer;
begin
try
i := StrToInt(Edit1.Text);
Label1.Caption := IntToStr(i+1);
except
Label1.Caption := 'That was not a number';
When an error arises, the Try/Exception block jumps to the Exception code and executes it.
If there are no exceptions, the Exception block never executes.
The complete source code to our form is:
unit Unit1;
interface
uses WebCore, WebUI, WebForms, WebCtrls, WebLabels, WebBtns,
WebEdits;
type
TForm1 = class(TForm)
Label1: TLabel;
Button1: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
procedure TForm1.Button1Click(Sender: TObject); var
i : integer;
begin
try
i := StrToInt(Edit1.Text);
Label1.Caption := IntToStr(i+1);
except
Label1.Caption := 'That was not a number'; end.
Let’s clean it up a bit. Going back to the Visual Form Designer, look to the left of the screen
and you will find the Object Inspector. Click once on the Label1 and the Object Inspector
will show its default values. Caption is set to Caption, change this to enter a number. This
changes the startup value of the Caption.
Click once on the button1 and it its Caption to Go. Now your application looks better. You
can drag the visual controls around the screen to make the position nicely.
There is only one other piece of code to write, and it is mostly hidden and usually
automatically written, it’s the project Application source.
Go to Project | View Source and you will find something like:
project Project1;
contains Unit1;
uses WebForms, WebCtrls;
begin
Application.Title := '';
Application.LoadProgress := False;
Application.CreateForm(TForm1); Application.Run;
End
We will explore this code later, but for now the important thing is that it creates
TForm1/Form1 (our form had a name), and then it runs the application, which is all the
open forms, of which there was simply Form1.
We will change the application again to show how live it is. Go to the System tab on the
Visual Form Designer and place a TTimer on the form. The TTimer is a non-visual
component meaning it doesn’t display anything. Double click on it and enter the following
code it its Timer1Timer event handler.
Using the Object Inspector you can change the Interval parameter from 1000 ms to 10000
for every 10 seconds. The beauty of EWB is that you write clean simple functions instead
of messy JavaScript which is error prone.
Our last change shows a useful tool for displaying answers and can help tremendously in
debugging: using ShowMessage() which pops up a message on the users screen
In our previous example, the time would overwrite the user’s results, and this is not ideal.
So change the code for the Button1 to:
procedure TForm1.Button1Click(Sender: TObject); var
i : integer;
begin
try
i := StrToInt(Edit1.Text);
ShowMessage( IntToStr(i+1 ), ‘debug’);
except
ShowMessage( 'That was not a number',’debug’); end;
end;
Run your app again. Now input generates a Modal message box that blocks all input until
the user presses Ok. Unlike in Windows or even alert() in JavaScript, this box does not halt
program flow, the program keeps executing but input is blocked. This behavior is due to the
Message Queue which we will examine later.
There are some things to note. EWB applications are SPA or Single Page Applications,
like Gmail, Facebook, etc. This means you load a page once and do everything without
loading subsequent HTML/JavaScript programs from the network. This makes the
experience fast and responsive to users. It feels like a Windows program. And now web
apps are as easy to write as Delphi Windows programs.
Your application can access the network, and we will do plenty of that in time, but
typically it will do AJAX (Asynchronous JavaScript And XML) calls to a server and
update itself as needed in the background.
Only the simplest apps have a single form, real apps usually have multiple forms that either
appear simultaneously or in some sequence.
With EWB you can have as many open forms as you please (and your JavaScript run-times
allow), easily in the hundreds if not more.
Each form type is written in a separate source file, however, you may create several
instances of the same form type, in which case all instances would be based on the same
file.
Simply take your single-form app as we had in the previous chapter and add more forms by
pressing File | New | Form and then selecting the base class for your Form. Make this one
of type TDialog..
Press F9 to run, and you will see there are two forms created, one will be over the top of
the other. You can grab the dialog box by its top and drag it off of your old Form1.
Our problem is that Form2 is being create and displayed along with Form1 by the
Application’s source. So fix it by editing Project | View Source
project Project1;
contains Unit1, Unit2; uses WebForms, WebCtrls;
begin
Application.Title := '';
Application.LoadProgress := False;
Application.CreateForm(TForm1);
Run the program and we only see the one form. Place a TLabel on Form2. It’s time to get
into the good habit of always changing your components to useful names, so go to the object
inspector and change its name property to: lbUserAnswer which stands for Label User
Answer.
i : integer;
begin
if not Assigned( Form2 ) then
Form2 := TForm2.Create( Application );
try
i := StrToInt(Edit1.Text);
Form2.lbUserAnser.Caption := IntToStr( i + 1 ); Form2.Show;
except
ShowMessage( 'That was not a number'); end;
end;
which indicates the module references things in the Interface section of the Unit2 file which
has our second form. Press F9 and your program will display its output in the new output
window.
The Form2 variable is defined as type TForm2 in Unit2, which we included in the uses
statement.
We use if not Assigned( Form2) because by default the object/TForm1 is not assigned any
memory – well, it was assigned, but we commented that out in the Project Source, so it
must be created manually.
Form2 := TForm2.Create(Application) creates the object and sets all the parameters to
their defaults, including all the parameters of all visual and nonvisual components of the
form.
Form2.Show shows the form, because merely creating and modifying a form isn’t enough,
you have to show it too.
- EWB generates a single web page (SWP) no matter how many forms are visible and how
many buttons one presses
- Forms are each designed in a separate unit
- Forms are auto-created in the Application source, you must comment them out if you do
not want them created and displayed at start time.
- You must instantiate each form variable with
TFormN.Create(Application).
- The only global variables we know about (so far) are Application, Form1 and Form2.
The rest are objects within these object classes. The next few chapters introduce you to
classes available in the Visual Form Designer. The online help is a complete and updated
reference; my goal here is to describe the controls and list their most noteworthy
parameters.
Among the classes you can drop in a form, the most common are from the Standard group.
They are almost all text-based, though some have optional icons.
The label is usually text, numbers, and other things which are being presented. These are
cornerstones of your visual form. You can update the Caption at any time to update the
display.
Like most controls, it is possible to edit the many object attributes. Some commonly
changed ones are:
• AutoSize – typically leave as true, otherwise you must adjust the size if you reassign the
Caption
• Caption – the text to display
• Font parameters, which include Color, GenericFamily and Size
• Background, which includes Fill… Color to highlight a text field
• Format … Wrap to turn on line wrapping
• Tag – which can hold an arbitrary number, useful for holding any data number you want
• OnClick() event handler.. yes labels can receive events, and OnDblClick() double click
handler.
• OnSize() is called when the size changes for any reason. This is a special label which has
a balloon holding the text and an optional icon property which specify one of a few
standard icons. You may choose to make it invisible by set Visible property to False at
design time, then set Visible := True to make it appear. Setting the Animations property you
can make it pop up nicely.
Yet another label extension. This one displays an alert at the top of the window.
AllowClose determines whether an X close button appears to close the error message or
not. Orientation determines whether a left sided or right sided X is used to close the error
message. It is common to create the TAlertLabel at design time but only make its Visible
property true when necessary, such as when the network request fails or a password is
incorrect.
mouse for helping the user with a balloon text or some other notice.
• Icon can specify an icon
The TIconButton specifies a button with an icon (glyph) and no text or border.
• Like most controls, you can specify a different cursor for when the mouse pointer is over
the control
• Sometimes you may want to fire events as long as a button is pressed, like in a video
game. Set the RepeatClick to True and
• If you set ModalCancel to True, this button will be called when the user presses Esc.
• If you set ModalEnter to True, this button will be called when the user pressed
Enter/Return.
• You must set ModalResult to the value which will be returned from the modal dialog box.
Use this!
• InputType can be set to various values. It does not restrict the input but
on many devices it determines what type of virtual keyboard is
displayed.
o tiNumber
o tiEmail
o tiNone
o tiURL
• It is possible to modify keystrokes entered by intercepting the
OnKeyPress or OnKeyDown. For example, you could make a sound if a
user entered a letter in a number field.
• It is common to hook the OnKeyUp() event handler to process
keystrokes after the Edit has placed them into the Value buffer.
This control is used just like TEdit, but it displays dots instead of characters.
MultiLineEdit is a self-explanatory name.
• The text entry is stored as a TStrings property called Lines. You can specify Lines[0],
Lines[1], etc. up to Lines.Count -1
• You can also read or set the whole string with Lines.Text
• MaxLength is the number of characters allowed
• ReadOnly is useful for transforming this to a text output box
• ScrollBars can be defined as sbHorizontal, sbVertical, sbNone or sbBoth. Scroll bars are
only displayed if necessary.
• The default View is cvMonth, you can also set it to cvCentury, cvDecade, and cvYear
• Read or write the date by editing the Text field
• Dates are stored in US American format: month/day/year, you may have to convert to your
locality’s format
• See also:TDateEditComboBox
• Text contains the current value, which is either empty string or one of Items
• Often handle OnCLick() or OnChange() events
TListBox is a box containing a list of strings.
The TDateEditComboBox is an Edit box which features a drop down calendar if the user
clicks on it.
• The date is stored in American format month/date/year
• Read or write the Text property
• Your program or the user can specify a time following the date and it will be viewable in
the Text property, but the built-in calendar only specifies a new date
• The application can read or write SelectedDate which is the DateTime selected by the
user but returns DateTime(0) if the date is invalid OR the user specified a time on that date.
SelectedDate is in the local time zone
This is an Edit/ComboBox which is useful for situations where a user needs to be able to
enter in a value or select it from a separate dialog, such as a lookup dialog that has a data-
bound grid in it.
RowSelect := True)
• Selected items are in a Selected array
• TGridCells are arranged in TGridColumns and TGridRows
• We will show several examples later
The graphics controls are… well... graphical.
Containers hold other visual controls. Sometimes they have visual boarders, but not
always.
A THeaderPanel is the header that goes at the top of a form or a panel. Typically you
would place a TLabel into the THeader which would say the name of the virtual window.
It is automatically included in a TPanel.
TSizer is used to allow the user to adjust the size of controls. See the chapter on layouts.
A TSizeGrip modifies controls so a user can resize them, simply drop the TSizeGrip onto a
control and you can drag the lower right hand corner larger or smaller, up to constraints in
the control’s constraints, or the size of the bounding control.
There is only one menu class, TMenu.
• Use OnClick event which is fired when someone presses a menu option
• Index is the currently selected item.
• Use the + symbol to add menu options.
• Set Visible to show or hide the menu
The TDataSetToolBar shows a perfect example of a TToolBar in use, it has buttons for
common actions like adding, deleting, modifying records and more. The TLink control has
a clickable caption which leads a browser to a different page.
The TScript component allows you to include a JavaScript file in your application
dynamically. You must still specify an external interface from the EWB compiler. We use
this extensively in our JavaScript chapter.
Implement a Google map into your application. Specify Locations on the map. Set Options
such as MapType for roads or other options. In our Cordova/PhoneGap example we
desmonstrate how to use the TMap.
You can design your web form using the WYSIWYG (what you see is what you get) visual
form designer.
EWB supports dynamic layout and sizing of controls known in the industry as Responsive
Web Design. It seems a little bit intimidating at first, but it is powerful and relatively
simple once you get started. After only a few projects, you will get the hang of the layouts
and many of us use it in every new project.
• LayoutOrder – the order in which objects are added among layout groups.
• Layout parameters:
o Consumption
o Overflow
o Position
o Reset (Boolean)
o Stretch
• Margins (left, right, top, bottom)
• Padding (left, right, top, bottom)
Objects are laid out in order specified by LayoutOrder. First 0, then 1, etc. and they are
placed into the remaining rectangle of space in the Form or container.
Position tells from where to start drawing the object. Often you will start at the TopLeft and
proceed either to the right or down. Sometimes buttons start at the bottom right and grow
leftward or up.
Consumption specifies if the direction the object grows (left, right, etc.) and eats into the
remaining rectangle.
Overflow says what to do when you run out of space in the current direction, like go Right,
go Down, etc.
Reset says to reset to the beginning since the last Reset (or the start).
Stretch tells whether to optionally grow in a direction, or not at all. If an object stretches,
its minimum and maximum constraints are observed. If it doesn’t stretch, they are ignored
because it stays the designed size.
Margin says how close an object should be to other objects, and padding is the space
inside the object reserved for spacing.
An example is needed to see how these factors all play together. This example is found on
the EWB web site under Responsive Design.
Thus LayoutOrder 1 is below it. And it grows to the right, but has a RESET=True, so the
rectangle goes to the last reset position which is at the top of the remaining rectangle.
LayoutOrder 2 is a tricky one: it stretches to the right (consumption), but grows toward the
bottom, so the remaining rectangle is below it.
LayoutOrder 3 naturally falls below LayoutOrder 3 because we grew to the bottom. Now
we grow to the right.
Where LayoutOrder 4 is placed depends on the remaining space. If there is room, we
continue growing to the right. But if you resize the rectangle smaller, #4 has to go below
#3, because #3 specified overflow would go below.
It is useful to stretch and squeeze the browser window. Also, many browsers support a
responsive design mode which simulates the size of common devices like cell phones,
tablets, etc.
Using responsive design, your pages can be attractive and functional on all devices. But
you will have to test and apply the rules as we have done here to make sure it grows and
shrinks the way you want.
When doing responsive design, you often want to have the MultiLineEdit box grow as
needed. This can easily be accomplished in code by setting the OnKeyUp handler as
follows;
c : integer;
begin
c := MultiLineEdit1.Lines.Count;
MultiLineEdit1.Height :=
(c+1) * (3*MultiLineEdit1.Font.Size) div 2; end;
Don’t let the math scare you, it’s just c (the number of lines of text) incremented by one so
there is always a bit of extra room, multiplied by a bit more than the font height.
Sometimes you want the form or panel to be resizable on the desktop.
Start by setting your form’s layout to TopLeft placement and stretch BottomRight. Then
place a TScrollPanel on your form , set its layout to TopLeft placement with strech
BottomRight, and finally set the Scroll property to ssBoth.
If you only want part of the form to scroll, use a TBasicPanel to delimit the area that will
not scroll, using Layout parameters to occupy that space. Then use the above steps with
TScrollPanel on the remaining space to create a scrollable area.
You can impose Contstraints that limit the minimum and maximum size of your scollable
area.
EWB supports {$DEFINE xxx} and {$IFDEF xxx}/{$ENDIF} much like Delphi.
You can use this to mark of a section of code that you want to temporarily disable. Often
you will have a problem with a function, so {$ifdef DEBUG} it out, and {$ifdef DEBUG}
out the definition in the TYPE section too.
Another great use is to comment out commens with embedded { and } characters, like
JSON or sample C/PHP/JavaScript code. You cannot normally comment out these things
because every end brace } frustratingly turns off comments.
I use {$ifdef xxx} between different versions of my code. Modern browsers allow you to
store some data locally in the local and session storage. The space is not enormous, but it
is big enough to store useful strings.
Local storage is conveniently accessed through the LocalStorage TPersistent class with
functions like Set, Items (which gets data), and Clear which all operate on Key names.
Local storage persists for a long time, possibly years.
Session storage is access through the SessionStorageƒ TPersistent class with the same
functions as LocalStorage. Session storage generally is deleted when you exit or restart
your browser (not just closing the window or tab).
Be aware that these data, although not visible to other Web applications, are not considered
secure. The user or local applications can read the data, so do not store anything sensitive
like Social Security Numbers, passwords, etc.
Session storage is particularly useful for Web site that have pages which load and change
frequently but EWB usually does not usually involve a lot of different web pages.
However, you may wish to store which windows are open, so if the user hits reload then
you can do something intelligent to recover.
Here is the source code:
unit storage1;
interface
uses WebCore, WebComps, WebUI, WebForms, WebCtrls, WebEdits,
WebLabels, WebBtns;
type
TForm1 = class(TForm)
edSession: TEdit;
edLocal: TEdit;
Label1: TLabel;
Label2: TLabel;
btSave: TButton;
btRetrieve: TButton;
Label3: TLabel;
Label4: TLabel;
btClear: TButton;
procedure btSaveClick(Sender: TObject);
procedure btRetrieveClick(Sender: TObject);
procedure btClearClick(Sender: TObject);
private
{ Private declarations } public
{ Public declarations } end;
var
Form1: TForm1;
implementation
end.
As we alluded to earlier, you can compile to JavaScript and debug that. And we will
discuss techniques built into EWB and also some not yet included in EWB, such as adding
breakpoints, using JavaScript variable watches, and more advanced DEWBUnit Unit
Testing is introduced.
The first step is to ensure you have not enabled compression/encryption on your project.
This way the generated HTML/JavaScript is human readable.
When trying to figure out raised exceptions, a method I found helpful for debugging
exceptions is to place a try / except block around code that has problems. Then declare a
local variable var ProgressNum : integer. In various places around your function, assign
ProgressNum to various different values (0,1,2,3…). And in your exception handler have
Except on Exception: E do
ShowMessage(‘ERROR: ‘+E.message +
IntToStr( ProgressNum ));
End;
This code fragment will pinpoint the last good spot in your program before the exception.
Note that EWB exceptions are based on JavaScript exceptions, which differ from Delphi
Pascal exceptions. For example any number div 0 in JavaScript does not generate a divide
by zero error, instead the returned result is infinity. If you depend on a divide by zero
exception, you will have to detect a zero and raise the exception yourself.
Another trick is to use ShowMessage() to show variable contents and how far you have
progressed. This has a problem under EWB in that the message boxes are all displayed
concurrently, so you will have a bunch of message boxes present.
Native JavaScript is typically riddled with bugs due to the fact the language is interpreted
rather than compiled and not strongly typed. For these reasons, almost every web browser
supports debugging features for JavaScript programmers. We’ll learn how to use them.
First, you must download my debugcode.js JavaScript file and add it to your project
(Project | Options | External Files | … ) Then add the following few lines to your interface
section.
Start by adding a breakpoint to any function where you are having problems. Just add the
line:
eval(‘debugger;’);
On most browsers, F12 enables the debug screen. If it doesn’t on yours, either find it on the
menu or switch to an easier-to-use debugger like Chrome or Firefox. F12 and debug
output/breakpoints do not work on the built-in browser of the EWB IDE.
Click on the “console” button on your browser’s developer pane, it should show no error
messages except the messages you outputted with debugoutput(). If you see errors here
there is something that should be fixed. You can output text to the console window with
console.log(‘stuf’), and this is a handy way to debug – output everything that happens until
something stops.
Step over the debugger statement and return to the calling function, which is typically
what you will want to debug. You can usually see the contents of any variables and classes
by clicking on their names inside the source code, and many browsers let you set a Watch
variable, just like Delphi’s debugger.
Many EWB programs have been written without the use of JavaScript debugging, but once
you see these tricks, you can speed up development by improving your efficiency
debugging your code.
Another way to set breakpoints is to edit the JavaScript output, search for some word in
your source code (constants, variable and class names are good candidates), and insert the
command debugger; where the trouble is, then run the application.
Unit Testing is the systematic creation of tests to validate software, it’s an important step in
making professional quality software.
The idea is simple, put your logic into subroutines or non-GUI class methods, then create a
sequence of tests and run them against your code and test the actual outputs against
expected outputs.
For example, look at the following two subroutines using DEWBUnit,
QEWBUnit’s assert.equal() method checks that the first two parameters are equal. If they
are, all is well, otherwise it logs the error that one has some other value.
Button1.Caption := 'ha';
assert.equal( one, 1 , 'one = 1');
assert.equal( animal,'dog', 'animal = dog');
assert.okay('happiness found');
assert.fail('homework');
raise exception.Create('divide by zero');
end;
QEWBUnit is told the time limit for this routine and all its results is 4ms (assert.timelimit)
and that the expected number of assertions is 3 (assert.expect).
Assert.equal passes for one = 1, but fails for animal = dog. Assert.ok logs ‘happiness
found’, and also logs the failure ‘homework’.
Finally, QEWBUnit logs the exception created and not handled by your code. All you need
to add this is copy QEWBUnit.* to your project and add the few lines:
end;
The code creates an instance of the Tester, then queues tests on a procedure, then on the
TestMethod method, and again on the initial procedure.
The Start function runs the tests in the queue. In the results below, I have checked the
“Show only failures” checkbox, and the results show only test2 failed, and which failures it
had.
Among the results are the number of milliseconds of each stage. This value is not
absolutely correct as the QEWBUnit code does consume some time doing its chores. In
general, you will find pure JavaScript typically take 50ms or less, whereas GUI operations
are often over 100ms to complete. Network operations can take 10ms to seconds or even
minutes.
Note the Assert parameter is different for each function called. And your function may
return before the whole operation is completed due to the asynchronous model – such as if
you have networking or GUI calls which happen asynchronously.
The results are green for success, red for failure, and orange for an incorrect number of
results when you set a count with Assert.expect().
In a production application you would define important functions and typical results, then
write the function to implement the results.
Eg.
procedure TForm1.TestMethod( assert : TEWBU_test); var
t : DateTime;
yesterday : DateTime;
lastweek : DateTime;
lastyear : DateTime;
begin assert.timelimit(100); // no way should it take 100 ms
t := now;
assert.equal( fn(t), ‘right now’ , 'test for right now');
yesterday := DateTime( Integer( t ) – 1000*24*60*60 );
assert.equal( fn(yesterday),’yesterday’, 'test yesterday');
lastweek := DateTime( Integer(t ) – 1000*8*24*60*60 ));
assert.equal( fn(lastweek),’last week’, ‘test last week’);
lastyear := DateTime( Integer(t ) – 1000*370*24*60*60 ));
assert.equal( fn(lastyear),’last year’, ‘test last year’);
end;
Now that you have write the function fn(x:DateTime). Typically you would leave the tests
available to test every component before every new product release; this will ensure small
changes do not break working code.
There are DUnit testers for a number of platforms. I looked at using a pure JavaScript
version, but instead got frustrated and decided to write DEWBUnit in pure EWB code to
integrate better this environment.
There are many reasons you might want to use Painting on the form, some include:
- Bar/pie/line graphs
- Schematics
- Simple maps
- Modifications to images (which we do in the FileSave example)
When you place a TPaint on your form, you create a surface called a canvas on which you
can draw using:
For example, to draw a box, set the canvas FillColor to a color of your choosing and call
FillRect( x, y, width, height), and it and all other things will be drawn in that color until
you specify a new color.
Call FillText( caption, x, y) to place text on the canvas using the current color.
Several line oriented functions follow a path which you can later fill or leave unfilled.
For example, when we are going to draw a pie segment, we start a path at the center, move
to and create an arc,, then ClosePath back to the centre, and finally we set a color and tell it
to fill.
// create an arc
Arc(CHART_COORD_X,CHART_COORD_Y
,CHART_RADIUS,StartAngle,EndAngle);
// finish the pie wedge ClosePath;
The following example shows how to create pie and bar graphs from the same data.
Place a TPaint surface on your form, make it quite large or the following code will clip
meaning it goes beyond the size of the TPaint and is therefore not displayed.
Add two TButtons, Button1 and Button2 in the upper left hand corner, these will each draw
a graph.
unit pie;
interface
uses WebCore, WebUI, WebForms, WebCtrls, WebPaint, WebBtns;
type
TForm1 = class(TForm)
Paint1: TPaint;
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject); procedure
Button2Click(Sender: TObject);
private
{ Private declarations }
procedure ClearChart;
public
{ Public declarations }
procedure DrawPie;
procedure DrawBars;
end;
var
Form1: TForm1;
implementation
const
CHART_COORD_X = 200; CHART_COORD_Y = 200;
CHART_LABEL_X = 700;
CHART_LABEL_Y = 100;
CHART_LABEL_HEIGHT = 30;
end;
procedure TForm1.DrawPie;
var
I: Integer;
Data: array of Integer=[12,23,34,45];
Colors: array of
TColor=
[clElevateLightRed,clElevateLightBlue,clElevateLightOrang
e,clElevateLightGreen];
Labels: array of String=['North','South','East','West'];
LabelX: Integer= 400;
LabelY: Integer= 100;
Total: Integer=0;
Angles: array of Double;
StartAngle: Double;
EndAngle: Double;
Begin
ClearChart;
// Total will be the sum total of all numbers in pie for
I:=0 to Length(Data)-1 do
Inc(Total,Data[I]);
// create a list of angles SetLength(Angles,Length(Data));
for I:=0 to Length(Data)-1 do
Angles[I]:=((Data[I]/Total)*PI*2);
StartAngle:=(-PI/2);
with Paint1.Canvas do
begin
StrokeStyle:=dsColor;
Font.Name:='Segoe UI';
Font.Size:=14;
Font.Color:=clElevateLightBlack;
TextBaseLine:=blTop;
StrokeColor:=clTransparent;
LineWidth:=1;
// cycle through the numbers drawing the pie for I:=0 to
Length(Data)-1 do
begin
EndAngle:=(StartAngle+Angles[I]);
with Paint1.Canvas do
begin
BeginPath; // start afresh
// move to center
MoveTo(CHART_COORD_X,CHART_COORD_Y);
// create an arc
Arc(CHART_COORD_X,CHART_COORD_Y
,CHART_RADIUS,StartAngle,EndAngle);
// finish the pie wedge ClosePath;
(LabelY+(CHART_LABEL_HEIGHT*I))+2); end;
StartAngle:=EndAngle;
end;
Paint1.Canvas.BeginPath; // reset subpath for canvas end;
procedure TForm1.DrawBars;
var
I : Integer;
x, y: double;
Data: array of Integer=[12,23,34,45];
Colors: array of
TColor=
[clElevateLightRed,clElevateLightBlue,clElevateLightOrang
e,clElevateLightGreen];
Labels: array of String=['North','South','East','West'];
LabelX: Integer= 400;
LabelY: Integer= 100;
MaxHeight: Integer=0;
begin
ClearChart;
// get the height for scaling
for I:=0 to Length(Data)-1 do
MaxHeight := max( MaxHeight, Data[I] );
with Paint1.Canvas do
begin
StrokeStyle:=dsColor;
Font.Name:='Segoe UI';
Font.Size:=14;
Font.Color:=clElevateLightBlack;
TextBaseLine:=blTop;
StrokeColor:=clTransparent;
LineWidth:=1;
end;
(LabelY+(CHART_LABEL_HEIGHT*I))+2);
end;
end;
DrawPie;
end;
procedure TForm1.Button2Click(Sender: TObject); begin
DrawBars; end;
end.
Unit: WebUI
Alpha
CompositeOperation
FillColor
FillGradient
Global Alpha (Transparancy) of canvas, default 1.0
How are source pixels combined with destination pixels during drawing operation, default
coSourceOver
FillPattern
FillStyle
LineCapStyle
LineJoinStyle
LineWidth MiterLimit
ShadowBlur
ShadowColor ShadowOffsetX ShadowOffsetY StrokeColor
StrokeGradient StrokePattern StrokeStyle
TextAlign
TextBaseLine
Pattern to use when FillStyle set to dsPattern One of: dsColor, dsGradient, dsPattern
How are lines terminated when drawn, default csButt, other options are csRound and
csSquare How are lines joined when they intersect, default jsMitre, others jsLevel,
jsRound
Line width for drawing (stroking) on the canvas Upper limit when LineJoineStyle is set to
jsMitre, default 10
How much blur, default is 0, which is crisp What color is the shadow if ShadowBlur is not
zero Horizontal offset of shadow, default 0
Veritcal offset of shadow, default 0
Line color when StrokeStyle is set to dsColor Line gradient when StrokeStyle set to
dsGradient Line pattern when StrokeStyle set to dsPattern
BeginPath
Add an arc along the current subpath.
Add a straight line and an arc to the current subpath Discard any existing subpath and begin
new subpath with no defined starting point.
BezierCurveTo Add a cubic Bezier curve to the path.
ClearRect Draw a transparent clBlack rectangle, does not affect current path Clip
ClosePath
Compute the intersection of the inside of the current path with the current clipping region
and use the smaller region as the new clipping region.
ConvertToDataURL
Close the path by adding a line back to the first point, then start a new
Path
DrawImage Fill
Draw an image onto the canvas
fill the current path of the canvas with the color, gradient, or pattern specified by the
FillStyle, FillColor, FillGradient, and FillPattern
FillRect
FillText
IsPointInPath
LineTo
MeasureText
MoveTo
QuadraticCurveTo
Rect
Rotate
Save
Fill the specified rectangle with the color, gradient, or pattern specified by the FillStyle,
FillColor,
FillGradient, and FillPattern properties.
Draw the specified text at the specified point with the font specified
by the Font property, and the color, gradient, or pattern specified by
the FillStyle, FillColor, FillGradient, and FillPattern properties.
Does the point fall within or on the edge of the current path?
Add a line segment to the current path starting at the current point.
Measure the length of the specified text if drawn with the current font settings
Move the current point to the specified location and begin a new
Subpath
Add a quadratic Bezier curve to the current path, and the new position will be the ending
point.
Add a rectangle to the current path of the canvas. The added rectangle has its own subpath
not connected to any other subpaths in the current path. After this
method is called, the current point on the canvas is set to the point specified by the first two
parameters
Scale
SetTransform
Stroke
StrokeRect
StrokeText
Transform Translate
Change the transformation matrix to scale the future X and Y drawing by these factors.
Directly set the
transformation matrix.
Stroke (draw) the current path of the canvas with the color, gradient, or pattern specified
by the StrokeStyle, StrokeColor, StrokeGradient, or StrokePattern properties. The
LineCapStyle, LineJoinStyle, MiterLimit and LineWidth properties control how lines
of the current path are drawn.
Stroke (draw) the specified rectangle with the color, gradient, or pattern specified by the
StrokeStyle, StrokeColor, StrokeGradient, or StrokePattern properties. The LineCapStyle,
LineJoinStyle, MiterLimit and LineWidth
properties control how the lines of the rectangle are drawn.
Draw the outline of the specified text at the specified point with the font specified by the
Font property, and the color, gradient, or
pattern specified by the StrokeStyle,
StrokeColor, StrokeGradient, or StrokePattern properties. The LineCapStyle,
LineJoinStyle, MiterLimit and LineWidth properties control how the lines that make up
the outline of the font characters are drawn. See online notes about condensed text if the
specified text does not fit within the specified width.
Move the following commands to the horizontal and vertical offsets specified, ie. slide the
image in a given direction.
I recommend ‘The JavaScript Bible’ which has details on using these thinly disguised
JavaScript functions.
Sometimes you must know a little bit about JavaScript or HTML5 to do what you want.
Such is the case with resizing images or manipulating them.
Here we have a little program with a ButtonComboBox whose five items are set to 100
percent, 50 percent, 33 percent, 25 percent and 200 percent. Its onChange property is set to
ButtonComboBox1Change;
We add a TImage Image1 used to load an image. We set its URL property to any picture of
our choosing. Its OnLoad property is set to Image1Load which resizes the image to its fully
loaded size and makes it invisitble.
TForm1 = class(TForm)
ButtonComboBox1: TButtonComboBox;
Image1: TImage;
Paint1: TPaint;
procedure ButtonComboBox1Change(Sender: TObject); procedure
Image1Load(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
external function eval( s: string) : object; implementation
context : THTMLCanvasRenderingContext2D;
paint, img : THTMLElement;
begin
paint := THTMLElement(
window.document.getElementById('paint'));
context := THTMLCanvasElement( Paint ).getContext('2d');
EWB does not use HTML IDs, so we set the clientID that in turn sets the HTML ID to
something recognizable, in this case ‘image’ and ‘paint’.
The variable Paint is set to the HTML element JavaScript finds with the ID paint.
The variable is set to the HTML element JavaScript with the ID image, but EWB uses the
container of the Paint1 object which is a DIV, so we have to get its one and only child,
which is the true paint.
From there, it’s a simple matter of getting a HTML5 2d drawing context, and then drawing
the image on using whatever size we want.
Providing attractive displays is not enough, we often need the ability to transfer files
between the web client and server, and the other direction too.
The ability to upload files is one of the important functions of web pages.
The power of EWB can lead you to forget that it is limited by the JavaScript restrictions,
one of which is that file access is severally crippled to prevent web sites from stealing
files from your hard drive.
It is possible to upload files to a web server using techniques similar to other web page
editors.
First, drop a Containers | THTMLForm onto your form. This will be the HTML form that is
uploaded to the web server. Only controls inside this form are uploaded.
Place a TFileComboBox into the THTMLForm, and also a Edit and a TButtton. If these are
not inside the THTMLForm the whole thing will fail.
Place a Web | TBrowser elsewhere on the form. It will hold the output from the web server.
Set HTMLForm1’s Output to Browser1 which you just placed on the form.
Now when you run your web page, you can select a file to upload, enter a text string in
Edit1, and press Button1 to upload the file. The WebBrowser1 will display the output from
the web page.
By default, HTMLForm1 will POST the data as a MultiPartForm, which is appropriate for
binary data. You can change this in the Encoding parameter to feTextPlain or
feURLEncoded, and in Method to any valid HTTP method.
To download a data file you act as though you are going to replace the current web page
with a new one. This causes the browser to do an HTTP/HTTPS GET of the specified
URL.
uses webdom; …
url := ‘report.php’;
s := url + ‘?report=transaction.csv’
window.location.replace( s );
Then the HTTP/HTTPS server script indicates through MIME (Multimedia Internet Mail
Extensions) that the file is not HTML but rather some other format, and that its filename is
whatever, and that the file should be downloaded and not displayed. If you do not indicate
that the file should be saved with a filename, it will instead open up in the appropriate
application (eg. PDF viewer, Excel, Word, etc.).
report.php :
header('Status: 200');
header('Content-type: text/csv');
$u = "answers.csv";
header("Content-disposition: attachment; filename=$u;");
If you wish, you can download the file in a Web | TBrowser, which will load the new file
into an HTML IFRAME simply by setting the TBrowser’s URL to the appropriate web
address. This has the benefit that you can use POST to download the file, and POST
arguments are hidden from web server logs unlike GET arguments.
EWB uses a totally event driven model where nothing ‘blocks’.
There is no sleep() function, no waiting for network events like a return to our
HTTP/HTTPS request, no waiting for user input. Instead we use a TTimer to schedule a
delay, a network request invokes HTTP/HTTPS request handler when the request fails or
success, keystrokes and other input generate event methods for affected components.
This is a change from Delphi, where ShowMessage() or MessageBox() would block until
the user accepted the results… we cannot block, we can only complete event handlers.
(The exceptions to this non-blocking are described under window.alert() and similar
functions of the JavaScript chapter.)
There is also no concept of a TThread or multithreading in normal EWB (though we
present a multithreading solution in a later chapter). But there are workarounds; we can
schedule several short programming segments to occur in sequence. This is accomplished
with the async keyword.
begin
for i := 1 to 10000 do begin
Edit1.Text := IntToStr( i );
for j := 1 to 1000 do
k := j;
end;
end;
It is a for loop that would update Edit1’s Text to the numbers 0 through 10,000. And since
there is an internal for j := loop, it would delay between each update. The for j loop is not
intended to provide a specific delay, it’s just an example of something keeping the cpu
busy.
Try it. It doesn’t work the way you thought. The only things that happen are that there is a
five second delay while it calculates, and then the number 10000 is displayed.
EWB executes all code, then schedules an update to the display when all changes are
made.
In this example, if you want to show intermediate results, the trick is to use async to
schedule future code to execute. Async saves the state of all parameters necessary to
complete the call sometime in the future.
begin
async UpdateOneEdit1( 0 );
end;
for j := 1 to 1000 do
k := j ;
if ( v < 1000 ) then async UpdateOneEdit1( v + 1 )
else HideProgress;
end;
In normal operations like generating a web request, you will call ShowProgress() and then
the request function without using async. The request will start the HTTP/HTTPS
connection then return to the event handler.
When the HTTP/HTTPS result is returned, your request handler is called, so you process
the data and then call HideProgress.
Tag is a property on every component that has no declared use, it’s usable by you for any
purpose you want.
For example, when you write a calendar app, you can use Tag to hold the datetime or an
event number of the calendar event you are displaying in a TLabel. Then when the user
clicks on the TLabel, you know exactly when it will happen.
Another popular use is to set a TEdit’s Tag := 0 in the OnShow event, and set it to 1 in an
OnChange event. Then you would know if the user changed any details, and which details
since the edit was available.
You can set the Tag to an ordinal in an array of objects or a TObjs or TStringList, so the
Tag will give an index to which object or string it references.
You are limited only by your imagination and the 52 bits of the signed Integer type.
Every environment seems to have a different format for storing datetimes. In EWB we use
the 52 bit integer to hold the number of milliseconds since the Epoch of January 1, 1970
UTC (universal time coordinates).
Very likely UTC is not your current timezone, so you often want to present local time to the
user, and read local time from them, but often servers store UTC time if they are expected
to serve a world-wide market.
There are many functions to convert between DateTime and other formats, usually integers
or strings:
YearOf( x : DateTime , optionalUTC :boolean ) returns the year as an integer. If you do not
specify the optionalUTC parameter, it defaults to false, meaning local time.
h The hour number (12-hour clock) with no leading zero hh The hour number (12-hour
clock) with a leading zero if the hour number is less than 10
H The hour number (24-hour clock) with no leading zero
HH The hour number (24-hour clock) with a leading zero if the hour number is less than 10
m The minute number with no leading zero
mm The minute number with a leading zero if the
minute number is less than 10
s The second number with no leading zero
ss The second number with a leading zero if
the second number is less than 10
tt The AM/PM designation for a 12-hour clock literal
DateTimeToISOStr() returns a standard ISO string which is computer parseable and yet
human readable. It is based on UTC time.
The ability to do animations can give your application a nice look. You can animate any
control, including containers as well as individual TButtons, etc.
The animations will occur whenever there is a change to the size or visibility of a control.
You do not have to schedule a TTimer to do the animation, animations are built-in.
The following minimalist program shows how to animate a TButton and TListBox. Every
time you press the TButton, the TButon and TListBox will grow 50% by slowly expanding.
unit main;
interface
uses WebCore, WebUI, WebForms, WebCtrls, WebLabels,
WebLists, WebBtns;
type
TForm1 = class(TForm)
ListBox1: TListBox;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
procedure EaseIn( Ctrl : TControl ); var
h : integer;
begin
ctrl.BeginUpdate;
h := Ctrl.Height;
with Ctrl.Animations.Height do begin
Duration := 1000;
Style := asQuadEaseIn;
end;
Ctrl.Height := h +(h div 2);
ctrl.EndUpdate;
end;
Nomally you would click on the Object Inspector and set the Animations as constants
rather than entering them in code.
asBackEaseOutIn Easing equation function for a back easing in: accelerating from zero
velocity.
Easing equation function for a back easing in/out: acceleration until halfway, then
deceleration. Easing equation function for a back easing out: decelerating from zero
velocity.
Easing equation function for a back easing out/in: deceleration until halfway, then
acceleration. asBounceEaseIn
asBounceEaseInOut
asBounceEaseOut
asBounceEaseOutIn
asCircEaseIn
asCircEaseInOut
asCircEaseOut
asCircEaseOutIn
asCubicEaseIn
asCubicEaseInOut
asCubicEaseOut
asCubicEaseOutIn
asElasticEaseOut
asElasticEaseOutIn
asExpoEaseIn
asExpoEaseInOut
asExpoEaseOut
asExpoEaseOutIn
asLinear
asNone
asQuadEaseIn
asQuadEaseInOut
asQuadEaseOut
asQuadEaseOutIn
asQuartEaseIn
Easing equation function for a quadratic easing in: accelerating from zero velocity.
Easing equation function for a quadratic easing in/out: acceleration until halfway, then
deceleration.
Easing equation function for a quadratic easing out: decelerating from zero velocity.
Easing equation function for a quadratic easing out/in: deceleration until halfway, then
acceleration.
Easing equation function for a quartic easing in: accelerating from zero velocity.
Easing equation function for a quartic easing in/out: acceleration until halfway, then
deceleration.
asQuartEaseOut
asQuartEaseOutIn
asQuintEaseIn
asQuintEaseInOut
asQuintEaseOut
asQuintEaseOutIn
asSineEaseIn
asSineEaseInOut
asSineEaseOut
asSineEaseOutIn Easing equation function for a quartic easing out: decelerating from zero
velocity.
Easing equation function for a quartic easing out/in: deceleration until halfway, then
acceleration.
Easing equation function for a quintic easing in: accelerating from zero velocity.
Easing equation function for a quintic easing in/out: acceleration until halfway, then
deceleration.
Easing equation function for a quintic easing out: decelerating from zero velocity.
Easing equation function for a quintic easing in/out: acceleration until halfway, then
deceleration.
Easing equation function for a sinusoidal easing in: accelerating from zero velocity.
Easing equation function for a sinusoidal easing in/out: acceleration until halfway, then
deceleration.
Easing equation function for a sinusoidal easing out: decelerating from zero velocity.
Easing equation function for a sinusoidal easing in/out: deceleration until halfway, then
acceleration.
There are a lot of choices. Most developers follow some theme rather than a random attack
of the GUI on the poor user. Use them tastefully. As we will see later, TGrid is very useful
in database applications. But sometimes you will use TGrid on its own, just as a
convenient way to display and maybe update data.
One noteworthy application is in generating an online receipt for a transaction. The grid
looks good, and it can have a variable number of rows and columns.
For our TGrid example, place a TButton on the form and a TGrid. The rest we can do in
code.
procedure TForm1.Button1Click(Sender: TObject); begin
end;
Columns[1].ControlType := ctEdit; Rows.Row[0].Value[0] :=
'Name'; Rows.Row[0].Value[1] := 'Erick';
Rows.Row[1].Value[0] := 'Org'; Rows.Row[1].Value[1] :=
'Engineering';
end; end;
We first check the column count, otherwise we’d keep appending columns every time you
press the button.
We set column[1] as editable so a user can change its values. Not so for column[0], it is
readonly.
A popular question that appears on the message board asks how to make color versions of
TButtons or TGrids.
The long answer is that you can create class decendant controls. That is truly the best
possible result, as these controls can then be added to the component palette and employed
in all your subsequent projects.
The source code to the component library is included with your EWB license, so you can
refer to many example components there.
But doing that takes a lot of coding and clicking and perfecting. Suppose you want a quick-
and-dirty solution which solves your particular problem without all the effort.
The first thing to do is pick the parent control on which you wish to base your new control.
Consider TButton, we’ll add a color to it so there is a new property called color of type
TColor.
unit tcolorbutton1;
interface
uses Webdom, WebCore, WebUI, WebForms, WebCtrls, WebBtns,
WebLabels;
type
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
procedure Form1Show(Sender: TObject); procedure
Button1Click(Sender: TObject);
private
{ Private declarations }
butRed, butGreen, butBlue : TColorButton;
function MakeColorButton( caption : string;
color : TColor ; myleft : integer ):TColorButton; public
{ Public declarations }
end;
TColorButton = class(TButton)
protected
fFillColor : TColor;
procedure InitializeProperties; override; procedure
UpdateInterfaceState; override; function getFillColor :
TColor;
procedure setFillColor(const col : TColor );
published
property FillColor : TColor read getFillColor write
setFillColor;
end;
var
Form1: TForm1;
implementation
procedure TColorButton.InitializeProperties;
begin
inherited InitializeProperties;
fFillColor := clRed; // default is some vibrant color end;
end;
// user application code
end;
procedure TForm1.Form1Show(Sender: TObject);
begin
butRed := MakeColorButton( 'red', clRed, Button1.Width + 10
+
Button1.Left );
butGreen := MakeColorButton( 'green', clGreen,
2*(Button1.Width + 10 ) + Button1.Left );
butBlue := MakeColorButton( 'blue', clBlue, 3*(Button1.Width
+
10) + Button1.Left );
end;
Looking at TForm, we declare three buttons (butRed, butGreen and butBlue) and a
MakeColorButton() function which creates the buttons parallel to a normal TButton we
dropped on the form.
private
{ Private declarations }
butRed, butGreen, butBlue : TColorButton; function
MakeColorButton( caption : string;
MakeColorButton just makes a regular button, but adds a FillColor which is new.
So now we just need a class that does everything TButton does, but adds a color
background. We declare it as a descendant of TButton class, and we add an fFillColor
value and two helper function getFillColor and setFillColor which get and set the
published property FillColor.
published
property FillColor : TColor read getFillColor write
setFillColor;
end;
We also define two protected override functions. The system calls these overridden
functions instead of the default functions of TButton of the same name.
procedure TColorButton.InitializeProperties;
begin
inherited InitializeProperties;
fFillColor := clRed; // default is some vibrant color end;
It calls the usual UpdateInterface of the TButton, but then reasserts the Background Fill
Color as whatever we’ve defined.
That’s it. Of course, you have to know that UpdateInterfaceState is called to get the
component to update itself. But now you know.
Creating color, bold, italics, underline and strikeout options on grids is quite easy.
The result looks like this
The trick is simply to create an OnCellUpdate event handler which adjusts the font
characteristics.
First, add a TGrid to your form, then use the OnShow event to add data to your Grid.
unit colorgrid1;
interface
uses WebCore, WebUI, WebForms, WebCtrls, WebGrids;
type
TForm1 = class(TForm)
Grid1: TGrid;
procedure Form1Show(Sender: TObject);
private
{ Private declarations }
procedure OnCellUpdate( Col : TGridColumn ;
ACell: TGridCell );
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
procedure TForm1.OnCellUpdate( Col : TGridColumn ;
ACell: TGridCell );
var
textcell : TGridTextCell;
newcol : TColor;
fo : TFont;
begin
textcell := TGridTextCell( ACell );
fo := ACell.Font;
case textcell.index of
0 : begin
fo.Color := clRed;
fo.style.italic := True;
fo.style.underline := True;
end;
1 : fo.Color := clBlue;
end;
end;
procedure TForm1.Form1Show(Sender: TObject); begin
with Grid1 do begin
end;
Columns[1].ControlType := ctEdit; Rows.Row[0].Value[0] :=
'Name'; Rows.Row[0].Value[1] := 'Erick';
Rows.Row[1].Value[0] := 'Org';
Rows.Row[1].Value[1] := 'Engineering';
First, add a TGrid to your form, then use the OnShow event to add data to your Grid.
procedure TForm1.Form1Show(Sender: TObject); begin
with Grid1 do begin
end;
Columns[1].ControlType := ctEdit; Rows.Row[0].Value[0] :=
'Name'; Rows.Row[0].Value[1] := 'Erick';
Rows.Row[1].Value[0] := 'Org';
Rows.Row[1].Value[1] := 'Engineering';
Notice that we hard code the values above. In practice you can either set them to variables
or use the database to query the value.
The trick to getting color is simply to create an OnCellUpdate event handler which adjusts
the font characteristics.
ACell: TGridCell );
var
textcell : TGridTextCell;
newcol : TColor;
fo : TFont;
begin
textcell := TGridTextCell( ACell );
fo := ACell.Font;
case textcell.index of
0 : begin
fo.Color := clRed;
fo.style.italic := True;
fo.style.underline := True;
end;
1 : fo.Color := clBlue;
end;
end;
TextCell.index contains the row number of the value, so this example bases color and font
information on the particular row. And remember, in the OnShow event we only applied
the OnCellUpdate handler to Column #1. In practice you could apply it to many columns.
Finally, you may want to base your decision of particular colors on other factors. For
example, if the data is N/A, or < 0, or whatever, you may want to flag data problems with
red.
Persistence is the topic of storing data and recovering it later. Serialization is the ability to
take an object, flatten it out to some intermediate format for sending over a network or
storing in a database. Deserialization is the inverse, taking the intermediate format and
converting it back into a usable object (though possibly in a different environment and/or
programming language).
1. We have an object holding some data, say a customer record, or an accounting record, or
an inventory item sitting on the server database.
2. We want to get the data from that object into a usable format on the Web client written
with EWB.’
3. To get to that client, the data must traverse the Internet in some intermediate format as a
series of bytes.
EWB knows how to convert data from structures into two common formats: JSON
(JavaScript Object Notation). We will see just how easy it is with the first example below.
unit readerwriter1a;
interface
uses WebCore, WebUI, WebForms, WebCtrls, WebEdits, WebBtns;
type
published
property ClientID : integer read fClientID write fClientID;
TForm1 = class(TForm)
MultiLineEdit1: TMultiLineEdit;
Button1: TButton;
procedure Form1Show(Sender: TObject); procedure
Button1Click(Sender: TObject); procedure Form1Create(Sender:
TObject);
private
{ Private declarations }
client : TClient;
Reader : TReader;
Writer : TWriter;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
procedure TForm1.Form1Create(Sender: TObject); begin
client := TClient.Create;
Reader := TReader.Create;
Writer := TWriter.Create;
end;
The data structure TClient is a bit special in that we have defined published properties
such as FirstName, with read and write properties that save the data into private data
properties. For convention sake, we name the private properties f…
When we start our application, the onCreate method creates an empty TClient variable and
a TReader and TWriter. We will use later
In the OnShow event hander we display the JSON of the TClient structure in a few short
lines:
Then the user is free to edit the JSON and press Button1 which reloads the TClient from
the MultiLineEdit1 text box.
By calling Form1Show(), the procedure rereads the JSON notation and updates it on the
display.
The output of this simple program looks like this:
You can see, edit and update the JSON and thus the TClient.
Now add an fBirthday to the private part of TClient, and Birthday to the published part.
Private
…
fBirthday : DateTime
Published
…
property Birthday : DateTime read fBirthday write
fBirthday;
When you rerun the program it displays the date as the number of milliseconds since
January 1, 1970 UTC. Eg:
"birthday": 1469207693068
If you prefer, you can change the default output to ISO date/time by changing
"birthday": "2016-07-22T17:24:37.585Z"
or something similar which is more human readable. But it may still be confusing because
the time is UTC zoned.
To the novice programmer it seems annoyingly redundant to specifiy private and published
properties. But there are several good reasons.
Published properties are used by TReader and TWriter. Having a private property such as
Phone number could exist without being copied to and fro in the JSON.
Another benefit is that you can have handlers which correct or interpret the data. For
example, many systems like to specify gender of someone. Depending on your location and
current views of gender identity, it may allow Male, Female and Unspecified, or maybe
other designations. But people often just enter M or F or U. So what if the system
automatically fixed the data to the long accepted formats.
..
fSex : string;
procedure LocalWriteSex( s : string );
Published
…
property Sex : string read fSex write
LocalWriteSex;
end;
Sometimes you know you will have arrays of items and will want to often code them as
JSON arrays – eg. party guests from your TClients. The easiest way to accomplish this
uses TCollections. But to do so, you must have TClient inherit from TColllectionItem
(which is a descendant of TPersistent) instead of TPersistent.
If you took the above examples and substituted TCollectionItem for TPersistent, everything
works identically.
Consider the case where we have people and their cousins, who are also people.
We will build a program which dumps a person and their cousins to persistent storage…
actually just to a MultiLineEdit box. And we will have a button which rereads that
MultiLineEdit box and reloads it into our structure. You can tell it works if you change the
spacing or the entries, and they revert back to EWB’s spacing after you hit the button.
unit persistarray1;
interface
uses WebCore, WebUI, WebForms, WebCtrls, WebEdits, WebBtns;
type
published
property name : string read fname write fname; property
cousins : TPeople read fcousins write fcousins;
end;
TPeople = class( TCollection )
private
fPerson : TPerson;
published
property person : TPerson read fPerson write fperson; end;
TForm1 = class(TForm)
MultiLineEdit1: TMultiLineEdit;
Button1: TButton;
procedure Form1Create(Sender: TObject); procedure
Form1Show(Sender: TObject); procedure Button1Click(Sender:
TObject);
private
{ Private declarations }
me : TPerson;
reader : TReader;
writer : TWriter;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
procedure TForm1.Form1Create(Sender: TObject); var
cousin : TPerson;
begin
me := TPerson.Create;
me.name := 'Erick';
me.cousins := TPeople.Create( TPerson );
writer.Initialize;
me.Save( writer );
MultiLineEdit1.Lines.Text := writer.Output; end;
procedure TForm1.Button1Click(Sender: TObject); begin
me.Free; // totally destroy previous copy to start fresh
me := Nil;
me := TPerson.Create;
reader.Initialize( MultiLineEdit1.Lines.Text ); me.Load(
reader );
Form1Show( Sender );
end;
end.
In our OnCreate routine we initialize me to be Erick and set two of my cousins. We also
create a TReader and TWriter for use later.
procedure TForm1.Form1Create(Sender: TObject); var
cousin : TPerson;
begin
me := TPerson.Create;
me.name := 'Erick';
me.cousins := TPeople.Create( TPerson );
{
"name": "Erick", "cousins": { } }
end;
AWriter.EndArray( Count > 0 ); end;
So we simply write out the property name (which we call items), start an array, cycle
through the array and write each member, then end the array.
In our situation, TPeople has only one field/array we want to persist, the items field. But if
there were other useful fields, we could use the inherited function to persist them. So for
future expansion we included the inherited function at the start.
The process of reading in the data is a little bit more complicated. When you look at the
above JSON, cousins is a member of TPerson returning a TPeople, and to TPeople we are
adding an array called items. So we need to extend TPerson and TPeople with some more
overrides.
And if the PropertyName is not one of the ones we are specifically looking for, we are a
good citizen and pass it along to the default handler.
PropertyName := AReader.GetPropertyName;
PropertyName := AReader.GetPropertyName;
LoadArray( AReader );
end else
result := inherited LoadProperty(AReader );
tempperson : TPerson;
propertyname : string;
begin
end;
The complete code is below. I’ve used {$ifdef} / {$endif} to comment out the code to
show the initial version and the complete version so you can see what needed to be added.
unit persistarray1;
interface
uses WebCore, WebUI, WebForms, WebCtrls, WebEdits, WebBtns;
type
{$DEFINE DEMO}
published
property name : string read fname write fname; property
cousins : TPeople read fcousins write fcousins;
{$IFDEF DEMO}
protected
function LoadProperty( AReader : TReader ):boolean;
override;
{$ENDIF}
TPeople = class( TCollection )
private
fPerson : TPerson;
published
property person : TPerson read fPerson write fperson;
{$IFDEF DEMO}
protected
procedure SaveProperties( AWriter : TWriter ); override;
function LoadProperty( AReader : TReader ):boolean;
override;
function LoadArrayElement( AReader : TReader ):boolean;
override;
{$ENDIF}
end;
TForm1 = class(TForm)
MultiLineEdit1: TMultiLineEdit;
Button1: TButton;
procedure Form1Create(Sender: TObject); procedure
Form1Show(Sender: TObject); procedure Button1Click(Sender:
TObject);
private
{ Private declarations }
me : TPerson;
reader : TReader;
writer : TWriter;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$IFDEF DEMO}
procedure TPeople.SaveProperties( AWriter : TWriter ); var
person : TPerson;
i : integer;
begin
end;
AWriter.EndArray( Count > 0 ); end;
PropertyName : string;
begin
result := False;
PropertyName := AReader.GetPropertyName;
PropertyName := AReader.GetPropertyName;
LoadArray( AReader );
end else
result := inherited LoadProperty(AReader ); end;
{$ENDIF}
begin
me := TPerson.Create;
me.name := 'Erick';
me.cousins := TPeople.Create( TPerson );
me := TPerson.Create;
reader.Initialize( MultiLineEdit1.Lines.Text ); me.Load(
reader );
Form1Show( Sender );
end;
end.
If you are planning to use persistence to deal with MySQL, you will want to exchange data
in MySQL’s time format, and to most people’s delight, the local timezone is used rather
than UTC.
I’ve used {$IFDEF DEMO} to mark the small bit of code you need to add to a class or to
its ancestor class.
Much of the code for for this example is stolen from the previous examples. Again, we
display the serialized version, and when you press the button, it deserializes it back to the
object.
unit persistime1;
interface
uses WebCore, WebUI, WebForms, WebCtrls, WebEdits, WebBtns;
type {$DEFINE DEMO}
published
property name : string read fname write fname; property
birthday : DateTime
TForm1 = class(TForm)
MultiLineEdit1: TMultiLineEdit;
Button1: TButton;
procedure Form1Create(Sender: TObject); procedure
Form1Show(Sender: TObject); procedure Button1Click(Sender:
TObject);
private
{ Private declarations }
me : TPerson;
reader : TReader;
writer : TWriter;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
TempType: Integer;
TempInstance: TObject;
dt : DateTime;
s : string;
TempShortTimeFormat : string;
TempShortDateFormat : string;
begin
TempType:=PropertyType(AName);
if (TempType <> TYPE_INFO_UNKNOWN) then
begin
if TempType = TYPE_INFO_DATETIME then begin
AWriter.PropertyName( AName );
dt := DateTime(GetProperty(AName));
TempShortTimeFormat :=
FormatSettings.ShortTimeFormat; // save it
TempShortDateFormat :=
FormatSettings.ShortDateFormat; // save it
FormatSettings.ShortTimeFormat := 'H:mm';
FormatSettings.ShortDateFormat := 'yyyy/M/d';
s := DateTimeToStr(dt);
FormatSettings.ShortTimeFormat :=
TempShortTimeFormat;
FormatSettings.ShortDateFormat :=
TempShortDateFormat;
AWriter.StringValue(s);
end
else inherited SaveProperty( AWriter, AName ); end;
end;
s := Areader.ReadString;
TempShortTimeFormat := FormatSettings.ShortTimeFormat; //
save it
TempShortDateFormat := FormatSettings.ShortDateFormat; //
save it
FormatSettings.ShortTimeFormat := 'H:mm';
FormatSettings.ShortDateFormat := 'yyyy/M/d';
dt := StrToDateTime(s);
FormatSettings.ShortTimeFormat := TempShortTimeFormat;
FormatSettings.ShortDateFormat := TempShortDateFormat;
SetProperty(PropertyName,dt );
end else
result := inherited LoadProperty(AReader ); end;
{$ENDIF}
procedure TForm1.Form1Create(Sender: TObject); var
cousin : TPerson;
begin
me := TPerson.Create;
me.name := 'Erick';
me.birthday := now;
me := TPerson.Create;
reader.Initialize( MultiLineEdit1.Lines.Text ); me.Load(
reader );
Form1Show( Sender );
end;
end.
When we save a class, the class calls SaveProperty for each property passing a name, so
we use an overload method on SaveProperty. We call PropertyType() to get the property
type of the property, and if it is a DateTime we handle it ourselves. Otherwise we just call
the inherited function to handle all other types. We need to change the default date and time
formatting properties, so we temporarily save their formats and return them again when we
are done.
TempType: Integer;
TempInstance: TObject;
dt : DateTime;
s : string;
TempShortTimeFormat : string;
TempShortDateFormat : string;
begin
TempType:=PropertyType(AName);
if (TempType <> TYPE_INFO_UNKNOWN) then
begin
if TempType = TYPE_INFO_DATETIME then begin
AWriter.PropertyName( AName );
dt := DateTime(GetProperty(AName));
TempShortTimeFormat :=
FormatSettings.ShortTimeFormat; // save it
TempShortDateFormat :=
FormatSettings.ShortDateFormat; // save it
FormatSettings.ShortTimeFormat := 'H:mm';
FormatSettings.ShortDateFormat := 'yyyy/M/d'; s :=
DateTimeToStr(dt); FormatSettings.ShortTimeFormat :=
TempShortTimeFormat;
FormatSettings.ShortDateFormat :=
TempShortDateFormat;
AWriter.StringValue(s);
end
else inherited SaveProperty( AWriter, AName ); end;
end;
The code to deserialize is also straightforward. We just check if the type is DateTime,
swap around the format strings, read in the value, and restore the format string.
PropertyName := AReader.GetPropertyName;
PropType:= PropertyType(PropertyName); if PropType =
TYPE_INFO_DATETIME then begin AReader.SkipPropertyName;
AReader.SkipPropertySeparator;
s := Areader.ReadString;
TempShortTimeFormat := FormatSettings.ShortTimeFormat; //
save it
TempShortDateFormat := FormatSettings.ShortDateFormat; //
save it
FormatSettings.ShortTimeFormat := 'H:mm';
FormatSettings.ShortDateFormat := 'yyyy/M/d'; dt :=
StrToDateTime(s);
FormatSettings.ShortTimeFormat := TempShortTimeFormat;
FormatSettings.ShortDateFormat := TempShortDateFormat;
SetProperty(PropertyName,dt );
end else
result := inherited LoadProperty(AReader ); end;
Creating database web applications is where EWB truly shines. This makes sense because
Elevate Software was traditionally a database technology company.
Since version 2.05, EWB supports multiple TDatabase components, how many, it’s
actually unlimited. Database is created automatically at application startup time for both
visual and non-visual projects. It keeps track of all datasets used by the application and
contains properties, methods and events for starting and commiting data transactions as
well as rolling them back.
TDataSet manages data between the application and the database for both visual and non-
visual projects. You typically have one TDataSet for each SQL table in use.
TDataSets can be dropped on the visual form at design time or created at runtime.
The important Columns property contains the column definitions for the dataset.
Database.LoadColumns loads the columns for the dataset directly from the database,
transparently marshalling the values through a JSON (JavaScript Object NotatioN) string
across a network connection for the user. TDataSet.LoadColumns similarly loads the
columns using a JSON format, but it is up to your application to provide that JSON string.
This provides a handy way of locally inserting values without having the baggage of a
whole database setup.
Rows of data are loaded from the server using Database.LoadRows, or
TDataset.LoadRows following the same server versus local
Once the TDataSet is loaded, you can navigate using the First, Prior, Next and Last
methods.
The TDataSet can also call Insert, Update and Delete to deal with various row operations.
Insert takes a parameter: Append:Boolean. If true, the TDataSet is extended at the end,
otherwise the new row is inserted at the current position. Remember to Save after your
insert.
Update prepares the current TDataSet row for an update. Call Save when done.
Delete deletes the current TDataSet row.
TDataset.Find is used to find a specific or nearest match. It can specify a case insensitivity
and a Locale insensitivity.
The nearest match requires an active sort on the database (Sorted = True).
If there is an active sort on the dataset, CaseInsensitive must match the SortCaseInsensitive
property.
TDataSet.Sort invokes the sorting if and only if Sorted is True and sort columns have been
specified for the dataset.
SortCaseInsensitive and SortLocaleInsensitive control how the sort is performed.
You only need call Sort after the sort directions are initially assigned for the columns.
Subsequent to the sort, any row operations will automatically maintain the correct order of
the active sort.
Our first database app will be a static local database on the Web client itself and is simple
enough to show the principles without getting into the challenges of network
communications and server configuration.
Create a new project. Drop a Database | TDataSet onto the form, it’s a nonvisual
component, so it can be placed anywhere. Name it People. It will be our list of people.
Double-click on People’s Columns property in the Object Inspector, this opens the
Columns editor. In the Columns Editor, press the + button to add a new column, it will be
called PeopleDataColumn1 by default. In the Object Inspector, change the column name to
PeopleID and the DataType to dtString. Repeat to add a column named PeopleName with
DataType dtString and Length 50.
Place two edit boxes on the form, name one edID and the edName. Set their DataSet
properties to People (to match our DataSet), and their DataColumns to PeopleID and
PeopleName respectively – do this by clicking the down arrow on the TEdit’s DataSet and
selecting people, and down arrow on DataColumn and selecting the appropriate column.
Add a Toolbars | TDataSetToolBar to the form and set its DataSet to People. Now click on
the main form and add the following OnShow handler and add
procedure TForm1.Form1OnShow(Sender: TObject); begin
Database.AutoTransactions := False;
People.Open;
People.LoadRows(
'{ "rows": [
{ "PeopleID": "1",
"PeopleName": "Hon" },
{ "PeopleID": "2",
"PeopleName": "Daniel" }]}', False); end;
Press F9 and you will have a navigable database application, albeit a minimalist one. You
can insert, delete, modify and search the records. Since there is no backend, all the data
changes are lost.
You can add additional records programmatically with the following code in a TButton
OnClick handler:
Let’s add a Master-Detail relationship. Add a TGrid, set its DataSet property to people,
and press F9. The TGrid automatically uses the correct column names and displays when
you run it.
Clicking on a name in the master table brings it into the current record field for the TEdits.
Change a field in its TEdit, hit update, and the master table reflects the change. The
TDataSet replaces large amounts of code you would think you might have to write.
If you typed in the previous example, you may have run into problems entering the JSON
code.
JSON is a popular standard for data exchange, not unlike XML. But it differs from XML in
that it is quite compact and fast for machines to parse. It is also relatively human-readable.
But entering it manually is a pain, you should never have to do that.
EWB supports database transactions, where you make a bunch of changes on the client
dataset and then can either commit the to the server or rollback the changes.
First set:
Database.AutoTransactions := False;
The code is basically:
Database.StartTransaction;
… do some operations …. if happy then
Database.Commit Else
Database.RollBack;
The transactions are not Database Locks, they do not prevent others from making changes.
They are only pertinent to the client dataset – ie. the dataset in memory.
There is an excellent example in the EWB samples, see the transactions folder.
Automatic transactions are a feature you can use if you set
Database.AutoTransactions := True;
When you use AutoTransactions, the simple act of inserting or editing a row in the database
locks the row in the client dataset until you commit with a Save operation. No transaction
code needs to be written – assuming your database supports EWB’s locking. Many
databases do not, so then you should turn off Automatic Transactions.
That remains true even if you have a more complicated setup, like a master/detail form.
When you start inserting or editing the master row, EWB starts a transaction that does not
commit until you save the master row. And during that time, any changes in the detail view
will be executed in a nested transaction.
The single call to Save on the master row will commit all nested changes at once.
I recommend re-reading/refreshing the data rows periodically as other users may have
changed them, and also EWB can get out of sync after making changes it doesn’t
understand. For example, when you create a new SQL record with SQL CREATE/Database
Insert, the key field (eg. Index number) does not get assigned until the database enters the
transaction, but it never updates the client. So you shoud refresh the client after every
operation, and otherwise after long periods of inactivity.
For this chapter we will only consider PHP databases using SQL. The reader is assumed to
know some PHP and SQL. We will construct a skeletal PHP program less than 100 lines
long that simulates a database server and logs the EWB-intended operations from the
JSON based RESTful database operations.
This database server code is in lieu of an official PHP database from Elevate which was
planned for release 2.05. My implementation is basic in many ways. The functionality it
lacks: is a long list
• localization
• datetime support
• sorting
• full UTF8 character support (non-alphanumeric characters may change)
• BLOBs
• SQL string length limiting
• Type validation
• Error checking
• Access controls
• Etc.
REST is a truly open standard of how to represent large collections of data objects over a
network connection. Using JSON (or XML on some other systems) over HTTP/HTTPS,
REST allows one to specify the CRUD (create, read, update, delete) operations of a
database. But it is truly stateless, you can reboot your server or replace it with a new
server, and the REST operations will still make sense.
EWB uses two REST HTTP verbs: GET which gets one or more entries (akin to SQL
SELECT), and PUT which batch uploads a transaction of one or more operations (such as
SQL’s UPDATE, CREATE, DELETE).
The rest of this section is optional, you can read it to learn, or you can skip to the
example.
All operations specify a Database name and a DataSet name to the server in the
$_SERVER variable and a request=operationname. Also, if you specify a Userid and
Password, they are transferred in the X-EWBUser and X-EWBPassword parameters of
$_SERVER.
Then it will contain a JSON array of the list of operations to be completed in the
transaction with a dataset. Each operation will have an opcode: 1 for INSERT, 2 for
UPDATE, and 3 for DELETE, as well as a list of before and after parameters specifying
the object in question before the operation and the values after the operation.
I think this represents the bare minimum client you would want to consider for an EWB
database application.
The form has an empty TGrid named Grid1, a TDataSet named DataSet1 and buttons names
btInsert, btUpdate and btDelete.
unit sqlclient1;
interface
uses WebDom, WebCore, WebUI, WebForms, WebCtrls, WebData,
WebGrids, WebBtns;
type
TForm1 = class(TForm)
DataSet1: TDataSet;
Grid1: TGrid;
btInsert: TButton;
btUpdate: TButton;
btDelete: TButton;
procedure Form1Show(Sender: TObject);
procedure btInsertClick(Sender: TObject);
procedure btUpdateClick(Sender: TObject);
procedure btDeleteClick(Sender: TObject);
private
{ Private declarations }
procedure Select_Query;
procedure CommitError(Sender: TObject; const ErrorMsg:
String);
procedure AfterCommit(Sender: TObject);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
Grid1.Dataset := Dataset1;
Grid1.NewColumn;
colgrid := Grid1.Columns[0];
colgrid.Header.Caption := 'StaffID'; colgrid.DataColumn :=
'StaffID';
Grid1.NewColumn;
colgrid := Grid1.Columns[1]; colgrid.Header.Caption :=
'Name'; colgrid.DataColumn := 'Name';
HideProgress;
ShowMessage( ErrorMsg , 'SQL Error');
if window.confirm('Do you want to retry the commit ?') then
begin
ShowProgress('Retrying commit...');
Database.RetryPendingRequests;
end;
end;
procedure TForm1.AfterCommit(Sender: TObject); begin
HideProgress;
end;
procedure TForm1.Select_Query;
begin
Dataset1.Params.Add('dept=Engineering');
Database.LoadRows( DataSet1 );
end;
end.
The most important setup details appear in the page’s onShow handler. The
AutoTransactions is new for 2.05 and eliminates the need to start and end a transaction to
do simple operations. BaseURL is the URL to our database server PHP application.
We also set the user name and the user password. These will be passed to our server. In a
modern OAuth2 environment, you would replace the password with a JSON Web Token
(JWT), but that’s another story.
Next in the function we add zero or more criteria to narrow the search/SELECT, by adding
“dept=Engineering” we are saying only Engineering users will be considered. Then we
load the DataSet.
Dataset1.Params.Add('dept=Engineering');
Database.LoadRows(DataSet1);
The rest of the operations are straightforward.
This code is very incomplete, it is meant to show you how to access the parameters to
produce SQL commands. To keep it simple, there little error checking, no data validation
or access controls in this sample, so it would be a nightmare to place on the big bad
Internet in its present form. This is an educational tool only.
In about 90 lines of boilerplating, and a few lines PHP to SQL conversion code you will
replace (functions starting with sql_...), this code can function with most EWB database
examples, including the transaction example.
It does not handle BLOBs (binary large objects) for things like pictures, but it can return a
URL and you can use that to reference a picture for download. simple.php <?php
function sql_select($user, $pass, $database, $dataset,
$inputargs)
{
$i = 0;
foreach ( $outputargs as $a=>$b) {
if ( $i++ > 0) {
$sqla .= ',';
$sqlb .= ',';
}
$sqla .= $a;
$sqlb .= "'$b'";
}
savelog( "SQL: INSERT INTO $dataset ( $sqla ) VALUES ( $sqlb
)");
return '';
}
{
return 1;
}
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=-=
=-=-=-=-=-=
define("EWB_OPERATION_UNKNOWN", 0);
define("EWB_OPERATION_INSERT", 1);
define("EWB_OPERATION_UPDATE", 2);
define("EWB_OPERATION_DELETE", 3);
header('Cache-Control: no-cache');
//header('Content-Type:vapplication/json; charset=utf-8');
function safetext( $src )
{
return( htmlentities( $src, ENT_QUOTES ));
}
function savelog( $msg )
if ( $f = fopen("log","a+")) { fwrite( $f, "$msg\n" );
fclose( $f );
}
}
function getpostdata()
{
$postdata = file_get_contents("php://input"); return
($postdata);
}
function rest_error($msg) {
header("EWB_HTTP_ERROR", "HTTP/1.0 500 $msg"); exit(0);
}
{
case 'rows':
$options = array();
if( ! isset($_REQUEST['dataset']))
rest_error('missing dataset');
$dataset = safetext( $_REQUEST['dataset'] ); if( !
isset($_REQUEST['database']))
rest_error('missing database');
$database = safetext( $_REQUEST['database'] );
foreach($_REQUEST as $directive=>$value) if(($directive !=
'database') && ($directive != 'dataset') && ($directive !=
'method'))
$options[safetext($directive)] = safetext($value); }
$output = sql_select($user, $pass, $database, $dataset,
$options);
break;
case 'commit':
$data = getpostdata();
$json = json_decode($data, TRUE);
$json2 = $json['operations'];
foreach($json2 as $operation) {
if( ! isset($operation['operation'])) rest_error('missing
operation');
$op = $operation['operation'];
if( ! isset($_REQUEST['database']))
rest_error('missing database');
$database = safetext( $_REQUEST['database'] ); if( !
isset($operation['dataset']))
rest_error('missing dataset');
$dataset = safetext( $operation['dataset'] );
$inputargs = array();
foreach ( $operation['beforerow'] as $a=>$b)
$inputargs[safetext($a)] = safetext($b);
$outputargs = array();
foreach ($operation['afterrow'] as $a => $b )
$outputargs[safetext($a)] = safetext($b);
switch($op) {
case EWB_OPERATION_INSERT:
?>
You can see we handle the two types of requests described ealier, rows for SELECT and
commit for all INSERT, DELETE and UPDATE commands.
This simple program produces an output log file called log in the local subdirectory
(assuming it has write permissions).
For this example output, I ran the web page which called rows to get a list of Engineering
people, and then I called UPDATE and DELETE by starting a commit, editing, deleting,
and closing commit.
You may wonder why we pass the userid and password (or its cousin the JWT token) to the
SQL function. As you build more complex systems, undoubtedly you will start using other
REST services to answer your queries, and you will need to either identify the user or pass
the OAuth2 JWT token. Until then, it doesn’t hurt to have an unused parameter present.
I would recommend that you reopen the dataset after closing any commit if you use
commits, or after you make any change if you use autocomits, because the master table can
get out of sync when a person makes nonsensical edits. For example, it allows you to set,
change or clear the person id number, which no database ever should allow, and as we saw
in the output above, unassigned values like StaffID of INSERTed records may be missing,
making the subsequent DELETE fail.
I built the JSON by hand in the request= rows response. Sometimes the PHP internal JSON
building library consumes too much memory and the process terminates, so building JSON
by hand avoids that problem.
For a production database you would replace the sql_... functions with your own. They
would clean up the arguments, test their sanity, and then test user permissions before
performing any database operation.
What is surprising to many developers from other technologies, when you write these
functions you are enabling them to be used by whomever and however that person may
choose. There is no guarantee the database request came from any specific line of your
code, it could be another developer, or it could be a hacker. Code wisely my friend.
SQL table JOINs allow you to SELECT and UPDATE on multiple tables at once.
Suppose you were showing a table of userids joined with addresses. You could
SELECT * FROM userids NATURAL JOIN addresses WHERE dept = ‘Engineering’
Likewise, you can update fields on the JOIN
UPDATE userids NATURAL JOIN addresses SET street=’king’ WHERE userid = ‘tomk’
You only need to differentiate tables during INSERT and DELETE operations. Normally
you will add the three A’s, Access control, Authorization and Auditing.
Only the simplest databases do not care for who is connected – say perhaps a phone book
app. But if one can enter or change data, usually some form of access control and
authorization is required to keep the bad people on the Internet from destroying your data.
Even read-only data is precious. If your customer list were exposed, your competitors
could steal your customers.
As mentioned previously, you will sometimes want to download a BLOB, such as a PDF
document, or a jpeg image.
While this code does not handle BLOBs, it can reference a URL. That might seem
awkward, wouldn’t the URL have to check the user’s permission to view the file? Not
really, you can specify a difficult-to-guess and impossible-toenumerate filename and that
will secure your files.
YouTube does exactly that with its videos. It generates a random URL with a packed
base64 encoding for each filename at store time. If another file exists with that name, it
tries other names until it finds a free one. But the namespace is so enormous, multiple hits
are rare.
Also, the odds of a person requesting the any correct filename are miniscule and can be
made infitessimal if we also check for excessively many failed attempts from a given IP
address and impose an exponential backoffs after n failures.
The code to accomplish this is relatively straightforward, but be sure you are using a good
random number generator, or else your filenames will be predictable.
ObjectPascal over JavaScript can only do so much on the client. Eventually you will want
to communicate with a server for:
- Database transactions
- Messaging/Email
- Any number of things not doable by the client directly
- The EWB environment includes built-in functionality to marshal data into a standard
format (JSON) and use protocols (HTTP/HTTPS) to connect to servers and ask them to do
things.
They can also perform RPC (remote procedure calls) where the Delphi code in the client
ends up calling a PHP function in the server, passing parameters to and fro.
The RPCs on the server can talk to other services, such as Google’s or Microsoft’s
network services, and perform all manner of tasks such as geolocation, scheduling,
messaging, etc.
Some of our examples follow the Elevate strategy directly, others are mashups of our own.
We will be sure to explain each.
Remember CORS (Cross Origin Resource Sharing) imposes limitations on your web page:
it means your web page only communicates with the server that is hosting the page. If you
intend other servers to be used, you must enable Cross-Origin Resource Sharing on both
the development site, and the production site. This is not specifically a EWB topic, it
applies to all web client technologies.
If data is going across a network, you really want it encrypted. The following functions can
help.
CheckHTTPS determines if either the connection is HTTPS, or if the connection is to
localhost (ie. the local machine) which means unencrypted is okay. function
CheckHTTPS: boolean;
var
hostname : string;
protocol : string;
begin
protocol := LowerCase(window.location.protocol);
hostname := LowerCase(window.location.hostname);
result := ( hostname = 'localhost')or( protocol = 'https:');
end;
MakeHTTPS makes the current page HTTPS unless it is on the localhost, in which case it
can stay unenecrypted.
procedure MakeHTTPS;
var
s : string;
hostname, port, search : string;
pathname , name : string;
begin
if not CheckHTTPS then begin
hostname := window.location.hostname; port :=
window.location.port; search := window.location.search;
pathname := window.location.pathname;
s := 'https:' + hostname;
if ( port <> '' ) then s := s + ':'+ port; if (pathname <>
'') then s := s + pathname; if ( search <> '' ) then s := s
+ search;
window.location.replace( s ); end;
end;
This first example does not follow Elevate’s standard exchange of data, but it is easy to
read and will set the stage for future examples.
We will define a protocol where our Object Pascal client send data parameters to a PHP
application’s function, and that function returns data variables to the client. This is an RPC
or Remote Procedure Call.
For our first simple example, we will get a message-of-the-day for a given user, so we
need to pass the userid, a password to verify it is truly our trusted user, and a function name
for the request.
Eg.
userid = ‘erick’
password = ‘happy’
method = ‘advice’ : remote function call
And we will get returned
result = 0 : meaning failure result = 1 : meaning success
and if result =1, we will be returned some advice in the variable wisdom. Eg.
wisdom = ‘the secret to life is EWB’
For our example, use a TEdit to hold a userid, a TPasswordEdit to enter a password, a
TButton to invoke the transaction, and a TLabel to hold the result. Here are the two main
EWB functions: We call GetWisdom and it has a callback called GetWisdomResults that
updates the TLabel.
procedure
TForm1.GetWisdomResults(results:Boolean;wisdom:string);
begin
if results then
Label1.Caption := wisdom
else
Label1.Caption := 'results not found';
end;
with WebRequest do
begin
OnComplete := GetWisdomRequestComplete;
RequestContent.Clear;
RequestHeaders.Clear;
URL:='loginmodule.php';
Method:=rmPost;
RequestHeaders.Values['Content-Type']:='text/plain';
RequestContent.Values['userid']:= userid;
RequestContent.Values['password']:= passwordtext;
RequestContent.Values['method'] := 'GetWisdom';
Execute;
end;
end;
This code allocates a TServerRequest if we don’t already have a previous one, sets the
request handler and marshals in the data and the method name.
When the server replies, the following code is invoked:
procedure TForm1.GetWisdomRequestComplete(Request:
TServerRequest);
var
rpcresults : TStringList;
begin
if (Request.StatusCode <> HTTP_OK) then begin
ShowMessage('Error: '+Request.ResponseContent.Text);
GetWisdomResults( False, '');
end
else
begin
rpcResults := TStringList.Create;
rpcResults.Text := Request.ResponseContent.Text;
else
{
$method = $data['method']; switch($method)
{
case 'GetWisdom': GetWisdom($data);
default: print "result=0\nerror=method $method not
found\n";
exit();
}
This code parses the POST data, checks for a method, and if we understand the method,
invokes it with the data.
Here is the parse_data() function:
function parse_data() {
global $HTTP_RAW_POST_DATA;
$z = array();
$s = $HTTP_RAW_POST_DATA; $xx = explode("\n", $s);
foreach($xx as $a=>$b) {
$z[$yy[0]] = $yy[1]; }
return ($z);
function GetWisdom($data) {
if(CheckPassword($data)) {
$res = 'Secret to life is EWB'; print "result=1\n";
print "secret=$res\n";
}
else
{
print "result=0\n"; }
Nice and simple. Note, it calls CheckPassword which would normally be an LDAP
lookup, or an OAuth2 call, or a database lookup, or a RADIUS call. We’ll just hard code it
for now:
function CheckUserid($data) {
if(isset($data['userid']) && isset($data['password'])) {
$userid = $data['userid'];
$password = $data['password'];
if(($userid == 'erick') && ($password == 'happy')) return
(1);
}
return (0);
}
I’m not recommending you follow this path, but it shows the steps involved if you choose
to roll your own protocol or support an existing system. And once you have one function,
like our GetWisdom() in PHP, adding more is trivial.
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
PasswordEdit1: TPasswordEdit;
Label1: TLabel;
Browser1: TBrowser;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
WebRequest : TServerRequest;
procedure GetWisdom( userid, passwordtext : string );
procedure GetWisdomResults( results : Boolean ; wisdom :
string );
procedure GetWisdomRequestComplete(Request:
TServerRequest);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=-==-=-=-=-=-=-=-=-=-=
procedure TForm1.GetWisdom( userid, passwordtext : string );
begin
if not assigned( WebRequest ) then
WebRequest := TServerRequest.Create( self );
with WebRequest do
begin
OnComplete := GetWisdomRequestComplete;
RequestContent.Clear;
RequestHeaders.Clear;
URL:='loginmodule.php';
Method:=rmPost;
RequestHeaders.Values['Content-Type']:='text/plain';
RequestContent.Values['userid']:= userid;
RequestContent.Values['password']:= passwordtext;
RequestContent.Values['method'] := 'GetWisdom';
Execute;
end;
end;
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=-==-=-=-=-=-=-=-=-=-=
procedure TForm1.GetWisdomRequestComplete(Request:
TServerRequest);
var
rpcresults : TStringList;
begin
if (Request.StatusCode <> HTTP_OK) then begin
ShowMessage('Error: '+Request.ResponseContent.Text);
GetWisdomResults( False, '');
end
else
begin
Browser1.DocumentText := Request.ResponseContent.Text;
rpcResults := TStringList.Create;
rpcResults.Text := Request.ResponseContent.Text;
if rpcResults.Values['result'] = '1' then
if results then
Label1.Caption := wisdom
else
Label1.Caption := 'results not found'; end;
end.
$z = array();
$s = $HTTP_RAW_POST_DATA; $xx = explode("\n", $s);
foreach($xx as $a=>$b) {
$z[$yy[0]] = $yy[1]; }
return ($z);
function CheckUserid($data) {
if(isset($data['userid']) && isset($data['password'])) {
$userid = $data['userid'];
$password = $data['password'];
if(($userid == 'erick') && ($password == 'happy')) return
(1);
}
return (0);
}
function GetWisdom($data) {
if(CheckPassword($data)) {
$res = 'Secret to life is EWB'; print "result=1\n";
print "secret=$res\n";
}
else
{
print "result=0\n"; }
}
$data = parse_data();
if( ! isset($data['method']))
print "result=0\nerror=No method specified\n";
else {
$method = $data['method']; switch($method)
{
case 'GetWisdom': GetWisdom($data); break; default: print
"result=0\nerror=method $method not
found\n";
exit();
}
}
?>
Setting Address0 to 3 indicates there are three Address lines. It’s not hard, but it’s not
great.
Unlike the standards, it’s not self-specifying. You need to make up and know what all the
parameters are.
EWB likes results in JSON format, and that solves these problems. Only minor changes are
needed to the client and server.
First, the server:
We will use the PHP json_encode() function which takes a PHP object as its parameter. So
first we will define the results type:
class result {
public $wisdom = "";
}
We will define a global $error set to null. If an error is found, functions just need to set
$error to a value and the PHP code will return an HTTP error with the error code returned
as the sole text.
$data = parse_data();
$result = new result();
if( ! isset($data['method'])) {
function GetWisdom($data ) {
global $error, $result; if(CheckPassword($data)) {
$res = 'Secret to life is EWB'; $result->wisdom= $res;
}
else
{
$error = 'bad userid or password';
}
}
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // boilerplate
function parse_data()
{
global $HTTP_RAW_POST_DATA;
$z = array();
$s = $HTTP_RAW_POST_DATA; $xx = explode("\n", $s);
foreach($xx as $a=>$b) {
$z[$yy[0]] = $yy[1]; }
return ($z);
global $result;
if(isset($data['userid']) && isset($data['password'])) {
return (1);
}
return (0);
}
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // updated GetWisdom
function GetWisdom($data ) {
global $error, $result; if(CheckPassword($data)) {
$res = 'Secret to life is EWB'; $result->wisdom= $res;
}
else
{
$error = 'bad userid or password'; }
}
} else {
print json_encode( $result );
}
?>
The major changes to the client program is that we use a TWisdom to hold the result set:
type
TWisdom = class ( TPersistent )
private
fwisdom : string;
published
property wisdom : String read fwisdom write fwisdom; end;
The data receiver is greatly simplified:
procedure TForm1.GetWisdomRequestComplete(Request:
TServerRequest);
begin
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
PasswordEdit1: TPasswordEdit;
Label1: TLabel;
Browser1: TBrowser;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
WebRequest : TServerRequest;
Wisdom : TWisdom;
reader : TReader;
procedure GetWisdom( userid, passwordtext : string );
procedure GetWisdomResults( results : Boolean ; errmsg:
string );
procedure GetWisdomRequestComplete(Request:
TServerRequest);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=-=-
procedure TForm1.GetWisdom( userid, passwordtext : string );
begin
if not assigned( WebRequest ) then
WebRequest := TServerRequest.Create( self );
with WebRequest do
begin
OnComplete := GetWisdomRequestComplete;
RequestContent.Clear;
RequestHeaders.Clear;
URL:='jsonserver.php';
Method:=rmPost;
RequestHeaders.Values['Content-Type']:='text/plain';
RequestContent.Values['userid']:= userid;
RequestContent.Values['password']:= passwordtext;
RequestContent.Values['method'] := 'GetWisdom';
Execute;
end;
end;
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=-= procedure TForm1.GetWisdomRequestComplete(Request:
TServerRequest);
begin
end
else
begin
Label1.Caption := wisdom.wisdom
else
Label1.Caption := 'results not found:' + errmsg; end;
end.
In this chapter we will deal with interfacing with JavaScript by loading JavaScript
modules, calling JavaScript functions, interfacing with the Document Object Model, and
learning how to translate code fragments into Object Pascal.
There are a number of reasons why you would want to call JavaScript routines.
•
•
•
•
•
In some cases, you will call JavaScript for the above reasons, other times you will choose
to implement the functionality in EWB Object Pascal. Personally, I prefer Object Pascal
for the reasons I outlined in the first chapter, even though the resulting code that executes is
in fact JavaScript.
For the rest of this chapter and any units you write which involve JavaScript, include
WebDom in your uses line. It contains the essentials needed to interface with JavaScript
and you will get random errors if you forget it.
Define your types in the interface section. Declare all types before you declare variables.
Things written in JavaScript are of class object according to EWB terminology, whereas
most EWB classes are of class TObject. JavaScript itself is a classless language, it’s an
object language.
Type
TDog = class( TObject ) public
petname : string;
end;
var
mydog : TDog;
begin
mydog = TDog.Create;
mydog.petname := ‘harry’; end;
which gives a similar but slightly different result than usual JavaScript: mydog := new
Object();
mydog.petname := ‘harry’;
In many cases the default class(TObject) will work, but there are some libraries which
will get upset because the mydog object is different due to the creation technique.
If you get an error and need to simulate the above JavaScript code, you have to do things
slightly differently. First, declare the class of no ancestor type, or of type object, either
way it will be a descendant of object. You will also have to declare it external.
Type
External TGlobalDog = class
Public
begin
myglobaldog := TGlobalDog(
eval( ‘new Object();’)); myglobaldog.petname := ‘harry’;
end;
EWB has two types we will use frequently in this chapter: variant and object. Object refers
to classes. Variant refers to any type: it can be an integer, string, object, etc.
When you need to access a JavaScript function, declare it external as we did above with
eval().
External function eval ( s : string ) : object;
If you do not care about the result of a JavaScript function, you can declare it as a
procedure rather than function.
Variant is the type of JavaScript variables if you are not concerned about the type, and cast
it to the type you want.
var
External myArg : variant; S : string;
Begin
S := string( myArg );
End;
If the external object is a class with methods and properties, declare them as such. Declare
the class of no particular type, or declare it of type object, which is the same thing. For
example, consider a JavaScript class we call library, declared to the EWB class TLibrary.
Type
External Tlibrary emit library = class
Public
Property name : string read; Property obj : integer read
write; Function init( arg1: integer;
Begin
Result := Library.init( 1, 3 );
End;
Note, EWB does not reference the global variable until your code does. So if you load the
library later, you must wait until it’s fully loaded before accessing the variables. The
easiest way to do this are to use TScript to load the library, then either call the library from
its onload handler, or set a flag during onload that indicates the library is available for use.
We’ll see an example of this flagging later in the OAuth 2.0 example.
Some of these preliminaries are difficult now, but they should become clearer with the
examples shown next.
The DOM is used to access parts of your web page to read, change, or create new ones.
You can also use the DOM to add JavaScript or CSS files if you wish. Google is your
friend to learn about the DOM.
To access the DOM, include the WebDom EWB module to your source file. It exposes the
external (meaning JavaScript) variable window of class type TWindow.
var
external window: TWindow;
And TWindow is defined by the EWB uses files as:
I will not normally show the whole classes, but this one is quite useful and I wanted to
pique your interest. The emit word means to use the following JavaScript class but refer to
it with an EWB name, usually starting with T for convention sake.
Some of the methods and values are specific to certain web browsers, eg. Orientation,
OuterHeight, etc.
What can you do with it? Almost anything!
You can call window.print and the page will ask the user if the current window should
print.
If you want to pop up a message and temporarily stop your code, call
window.alert(‘some message’). It creates a modal dialog.
You can see that window.screen returns a TScreen, which is defined as:
external TScreen emit Screen = class
public
{ Properties }
property availHeight: Integer read; property availWidth:
Integer read; property colorDepth: Integer read; property
height: Integer read; property width: Integer read;
end;
So you could use that to see the color depth, the screen height, etc.
Look at THTMLElement:
If you are given a TDOMElement for an HTML element, you can cast it to THTMLElement
and then call the THTMLElement methods or access its properties.
For example, suppose you were given an HTML <div id=’qunit’></div> tag pair
and wanted to insert a message between them:
The JavaScript way would be to call the DOM;
Suppose you wanted to create that ‘qunit’ DIV on the fly, you could use
window.document.body.appendChild and window.document.createElement.
Procedure createqunit;
Var
ele : TDomElement;
html : THTMLDocument;
begin
ele := window.document.createElement('div');
ele.setAttribute('id','qunit');
html := THTMLDocument(window.document );
html.body.appendChild( ele );
end;
If you wanted to load an external CSS file, you could do that by extending the HEAD DOM
element.
Procedure loadqunitcss;
Var
css : TDomElement;
html : THTMLDocument;
url : string;
begin
url := 'qunit.css';
css := window.document.createElement('link');
css.setAttribute('rel', 'stylesheet');
css.setAttribute('type', 'text/css');
css.setAttribute('href', url);
html := THTMLDocument(window.document );
html.body.appendChild( css );
end;
While you could do something similar to our loadqunitcss to load a JavaScript file, I prefer
to use the EWB strategy and either have a GUI created TScript, or call TScript when you
need it with:
Procedure Form1.loadqunitscript; begin
script := TScript.Create( self );
script.onerror := script1error;
script.onload := script1load;
script.url :='qunit.js';
end;
And then define script1error which displays an error, and script1load which can then call
the loaded script.
In JavaScript redirecting to a different page is accomplished with
window.location.href = ‘http://google.com’;
EWB Object Pascal is almost identical:
window.location.href := ‘http://google.com’;
It is common to include libraries for specific functionality because including a library is a
lot easier than recoding it in EWB ObjectPascal.
In general, you will usually call either global procedures/functions, or deal with JavaScript
classes.
We will work with the Clippy.JS library that provides simulated Microsoft Office Clippy
characters. See the documentation and demonstrations at: https://www.smore.com/clippy-
js
A typical use would load clippy with the EWB external libraries code, or a TScript
strategy as discussed in the previous chapter. You will also need to have it load a JQuery
library using the same technique.
To do that from EWB, you need to define a subset of the class. You don’t need the whole
class, just the methods and attributes we will access. And you declare the global variable
of the class.
Type
end;
// we define the global function TYPE Tclippyproc =
procedure( agent : TAgent );
Sometimes you need a form of RichEdit to accept user input. There are several free and
several commcerical ones available. I picked CK Editor as it has support for spellchecking
in many languages and an obscene number of plugins.
To use CK Editor you place a normal MultiLineEdit on the page at design time, we’ve
named ours MultiLineEdit1, and we will tell CK Editor to replace the MultiLineEdit1 with
its own editor at run time.
We’ve also created an HTML DIV which will hold the rich text we create, and placed it to
the side. When you run your program it will look like this:
unit richeditor1;
interface
uses Webdom, WebCore, WebUI, WebForms, WebCtrls, WebComps,
WebEdits, WebLabels, WebBrwsr;
type
TForm1 = class(TForm)
Script1: TScript;
MultiLineEdit1: TMultiLineEdit;
Browser1: TBrowser;
procedure Script1Load(Sender: TObject); procedure
MultiLineEdit1Change(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
implementation
MultiLineEdit1.clientID := 'Edit1';
CKEDITOR.replace('Edit1');
text1 :=
'<h1>Hi There</h1><p>Welcome to <i>our</i> editor.</p>';
eval('CKEDITOR.instances.Edit1.setData("'+ text1+'" );');
p := MultiLineEdit1Change;
eval('CKEDITOR.instances.Edit1.on("change", p );');
MultiLineEdit1Change( Nil ); end;
procedure TForm1.MultiLineEdit1Change(Sender: TObject);
begin
browser1.DocumentText := string(
variant( eval('CKEDITOR.instances["Edit1"].getData();')));
end;
end.
In order to tell CK Editor to replace the default MultiLineEdit with itself, it needs to know
MultiLineEdit1’s HTML ID. EWB does not normally set IDs, but we can set it with
clientID. So we say:
MultiLineEdit1.clientID := 'Edit1';
CKEDITOR.replace('Edit1');
Next we want to set the data string, so we call setData(). We could have defined the whole
Pascal objects, but it was simpler just to call JavaScript’s eval() routine and let it deal
with the complicated mess.
We need to update the screen after each keystroke or mouse change, so we want to set
CKEditor’s Change function. You would think we could specify a typical function to do
this, but EWB changes the functions names, especially when you compress your output. So
first we set variable p to the name of the function we want to call – EWB automatically
knows the correct function name at compile time, so it fills it in. Then we assign the
Change event to p at run time.
p := MultiLineEdit1Change;
eval('CKEDITOR.instances.Edit1.on("change", p );');
MultiLineEdit1Change( Nil );
We call MultiLineEdit1Change to update the DIV right away, so it displays the supplied
text the instant one opens the web page.
MultiLineEdit1Change() just copies the getData() data from the CK Editor into the DIV. But
remember I was lazy and did not create all the structures but just called eval(), well eval()
returns an object but I need a string. I cannot cast an object to a string, but I can cast it to a
variant, and variants can be cast to strings. So we get this nasty casting that throws out all
the protections of EWB but convinces the compiler it makes sense.
You may wonder why I have Sender : TObject as a passed parameter. Again, laziness. I
used MultiLineEdit1’s OnChange default handler to create this. Unlike some programming
enviroments where passed parameters must be cleaned up by the function, JavaScript (and
thus EWB) can be passed any number of unused parameters. JavaScript is just that way.
EWB creates applications with the speed and flair of real applications. But traditionally
JavaScript could not read local files as that would be a security hole. That changed with
the File Reader. Now you can have the ability to load local files into your application and
process the data any way you like. An example of this would be to have a YouTube-like
uploader which sends files upstream and gives a visual indicator of the progress.
For our example we will ask the user for a file to read, then load it into a multiline edit
control. Obviously text files would look best in that editor.
Since the user may load a large file, we display the EWB busy message while the file
loads. In fact, try loading a huge file like a movie, it will take forever but at least the busy
message explains why.
TForm1 = class(TForm)
HTMLForm1: THTMLForm;
FileComboBox1: TFileComboBox;
Timer1: TTimer;
Label1: TLabel;
MultiLineEdit1: TMultiLineEdit;
Label2: TLabel;
Label3: TLabel;
procedure Timer1Timer(Sender: TObject);
private
{ Private declarations }
procedure StartReading( f : TFileType ; r : TFileReader);
function readSingleFile( evt : TMyEvent):boolean;
public
{ Public declarations }
end;
end;
end;
var
Form1: TForm1;
external function eval( arg : string ) : object;
implementation
function myreader( x : TArgFileReader ):integer; var
contents : string;
begin
contents := x.target.result;
HideProgress;
Form1.MultiLineEdit1.Lines.Text := contents ;
Form1.MultiLineEdit1.Visible := True; result := 0;
end;
begin
Timer1.Enabled := False;
// set the ID of FileComboBox1 to FileComboBox1
FileComboBox1.ClientID := 'FileComboBox1';
ele := TDOMElement(window.document.getELementByID(
'FileComboBox1'));
ele.addEventListener('change',readsinglefile, False );
FileComboBox1.Visible := True;
end;
end.
Some explanation is necessary. Look at Timer1Timer event. First we cancel the timer so
we don’t get subsequent events. The EWB normal onchange handler does not pass the
Event, just the Sender, but we need the Event from from the FileComboBox1 change event
as it contains the file information. So we need to call addEventListener to tell the DOM to
call our readsinglefile routine in the event of a filename change.
That introduces a second problem, DOM likes to refer to elements by their ID, but EWB
does not use ID’s and so doesn’t bother setting them. So first we set the ID by setting the
clientID = ‘FileComboBox1’. Then we can use getElementByID(‘FileComboBox1’) and
make our changes.
ReadSingleFile could contain all the code of StartReading, but we call it Async so that
ShowProgress() can start first and Label1.Caption will be displayed.
MyReader is called when the file loading is complete, so it does something with the data
and clears the progresss meter.
For production code you would need to better test with try/except to detect browsers that
cannot handle the reader. We left them out of this sample to keep it shorter.
This example lets you select a local file and then upload it using JavaScript. We will
indicate the filename and filesize while uploading.
unit fileuploader1;
interface
uses WebDom, WebHttp, WebCore, WebUI, WebForms, WebCtrls,
WebBrwsr, WebEdits, WebBtns, WebComps,
WebLabels;
type
TForm1 = class(TForm)
HTMLForm1: THTMLForm;
FileComboBox1: TFileComboBox;
Timer1: TTimer;
Label2: TLabel;
Label3: TLabel;
procedure Timer1Timer(Sender: TObject);
private
{ Private declarations }
HttpRequest : TServerRequest;
fname : string;
fsize : integer;
function myreader( x : TArgFileReader ):integer; procedure
StartReading( f : TFileType ; r : TFileReader); function
readSingleFile( evt : TMyEvent):boolean; procedure
UploadComplete( sr : TServerRequest ); procedure UploadFile(
base64 : string );
public
{ Public declarations }
end;
public
property name : string read; property size : integer read;
property type : string read;
//
end;
external TFileReader = class
public
property onload : TArgFileReaderFn read write; procedure
readAsText(f : TFileType); procedure readAsDataURL( f :
TFileType );
end;
var
Form1: TForm1;
external function eval( arg : string ) : object;
implementation
const
theurl = 'PostBase64.php';
with HttpRequest do
begin
method := rmPost;
onComplete := UploadComplete;
RequestHeaders.Clear;
RequestContent.Clear;
url := theurl;
RequestHeaders.add('Content-Type: application/x-www-
formurlencoded');
RequestContent.add('imagedata=' + base64 );
RequestHeaders.add('Content-Length: ' + IntToStr( Length(
RequestContent[0])));
execute;
end;
end;
begin
contents := x.target.result;
HideProgress;
ShowProgress('Uploading File :' + fname +', '+IntToStr(
fsize)
+ ' bytes');
UploadFile( contents );
result := 0; end;
begin
Timer1.Enabled := False;
FileComboBox1.ClientID := 'FileComboBox1';
ele := TDOMElement(
window.document.getELementByID('FileComboBox1'));
ele.addEventListener('change',readsinglefile, False );
FileComboBox1.Visible := True;
end;
end.
We use the readAsDataURL() method which reads the file and converts it to a Base64
notation then calls myreader().
Base64 is quite compact, it store 6 bytes of data in every 8 printable characters. We will
see Base64 again in the Camera examples under Cordova.
function TForm1.myreader( x : TArgFileReader ):integer; var
contents : string;
begin
contents := x.target.result;
HideProgress;
ShowProgress('Uploading File :' + fname
+', '+IntToStr( fsize) + ' bytes'); UploadFile( contents );
result := 0;
end;
myreader is given the contents of the file in base64 and calls the UploadFile() function
after informing the user of our activity.
with HttpRequest do
begin
method := rmPost;
onComplete := UploadComplete;
RequestHeaders.Clear;
RequestContent.Clear;
url := theurl;
//need to specify the content type
RequestHeaders.add(
'Content-Type: application/x-www-form-urlencoded');
RequestContent.add('imagedata=' + base64 );
RequestHeaders.add('Content-Length: '
So it does some GUI stuff to indicate the upload worked or it didn’t. Usually servers
discard files larger than an arbitrary size, and they may not handle this failure gracefully.
So your app should double-check that the file was copied, and you can even make
decisions based on the fileszie before you upload. Eg. ‘Hmmm, 33GB, no, that’s not going
to work.’
The disadvantage of this upload strategy is that the file must be able to be copied to
memory, in expensive Base64 sizing.
Before we continue, here is the PHP upload code. This is a very simple version, you will
need to add error checking and file storage, and you can look at the MIME type which I just
discard here. I assume I can write to the local subdirectory and write the data to junk.txt
(Base64 encoded) and junk.png (binary). Don’t be fooled by the png extension, you can
upload any file using this.
PostBase64.php
<?php
$imageFilename = 'junk';
$img = $_POST['imagedata'];
file_put_contents( "$imageFilename.txt", $img );
?>
$offset is the offset of the end of the header as it always ends in a comma. So we construct
a string of the remaind of the incoming string after the comma. Our Uploader2 sample
builds on the previous examples by cutting the file into manageable segments or chunks and
uploading each segment. After each segment is uploaded, we can update our progressbar so
the user knows something is happening. And by using smaller segments, we get around the
maximum upload size limit of the PHP server. On the downside, both the client and server
must still have enough available RAM to hold the complete file in Base64 encoding, though
we will explain how to alleviate that server problem in the followup text..
SEGMENTSIZE can be any number of bytes for each segment. In this example we set it
incredibly low, at 512 bytes each just to show how it works. The TCP/HTTP setup for
each segment eats into performance and adds to the HTTP server logs. In reality, TCP and
HTTP/S get more efficient with larger byte sizes, so you will want to tradeoff between
efficiency bigger sizes, and user updates which are less frequent with bigger sizes. I
suggest setting SEGMENTSIZE to 16kB or 64kB.
You will need to add a ProgressBar1 to your form. I suggest setting the ProgressBar’s
Background FillColor to something other than White, maybe a dull gray.
unit fileuploader2a;
interface
uses WebDom, WebHttp, WebCore, WebUI, WebForms, WebCtrls,
WebBrwsr, WebEdits, WebBtns, WebComps,
WebLabels, WebProgs;
type
TForm1 = class(TForm)
HTMLForm1: THTMLForm;
FileComboBox1: TFileComboBox;
Timer1: TTimer;
Label2: TLabel;
Label3: TLabel;
ProgressBar1: TProgressBar;
Label1: TLabel;
procedure Timer1Timer(Sender: TObject);
private
{ Private declarations }
segment : integer; // 0=EndOfFile, 1 is first, 2, 3, ...
contents : string; // moved to HERE to store between
HttpRequest : TServerRequest;
fname : string;
fsize : integer;
function myreader( x : TArgFileReader ):integer; procedure
StartReading( f : TFileType ; r : TFileReader); function
readSingleFile( evt : TMyEvent):boolean; procedure
UploadComplete( sr : TServerRequest ); procedure UploadFile;
public
{ Public declarations }
end;
//
end;
external TFileReader = class
public
property onload : TArgFileReaderFn read write; procedure
readAsText(f : TFileType); procedure readAsDataURL( f :
TFileType );
end;
var
Form1: TForm1;
external function eval( arg : string ) : object;
const
SEGMENTSIZE = 512;
implementation
procedure TForm1.UploadComplete( sr : TServerRequest );
begin
if segment > 0 then begin
inc( segment );
UploadFile;
end else begin
HideProgress;
if (sr.StatusCode <> HTTP_OK) then
ShowMessage('Error: '+sr.ResponseContent.Text, 'Results')
else
ShowMessage('Done','Results');
end;
end;
const
theurl = 'PostBase64Seg.php'; // handle segments
procedure TForm1.UploadFile;
var
base64 : string ; // now compute this
len : integer;
begin
with HttpRequest do
begin
method := rmPost;
onComplete := UploadComplete;
RequestHeaders.Clear;
RequestContent.Clear;
url := theurl;
//need to specify the content type
RequestHeaders.add(
'Content-Type: application/x-www-form-urlencoded');
RequestContent.add('imagedata=' + base64 +
'&segment='+IntToStr(segment) );
RequestHeaders.add('Content-Length: ' +
IntToStr( Length( RequestContent[0])));
execute;
end;
end;
result := 0; end;
begin
Timer1.Enabled := False;
FileComboBox1.ClientID := 'FileComboBox1';
ele :=
TDOMElement(window.document.getELementByID('FileComboBox1'));
ele.addEventListener('change',readsinglefile, False );
FileComboBox1.Visible := True; end;
end.
result := 0; end;
We save the Base64 text into the Form’s contents variable, initialize the segment to 1 and
call UploadFile which now has no arguments.
UploadFile is called repeatedly, and is the workhorse of this version.
procedure TForm1.UploadFile;
var
base64 : string ; // now compute this
len : integer;
begin
end;
with HttpRequest do
begin
method := rmPost;
onComplete := UploadComplete;
RequestHeaders.Clear;
RequestContent.Clear;
url := theurl;
//need to specify the content type
RequestHeaders.add(
'Content-Type: application/x-www-form-urlencoded');
RequestContent.add('imagedata=' + base64 +
'&segment='+IntToStr(segment) );
RequestHeaders.add('Content-Length: ' +
IntToStr( Length( RequestContent[0])));
execute;
end;
end;
UploadFile;
end else begin
HideProgress;
if (sr.StatusCode <> HTTP_OK) then
ShowMessage('Error: '+sr.ResponseContent.Text, 'Results')
else
ShowMessage('Done','Results');
end; end;
switch ( $seg ) {
case 1 : // new file
file_put_contents( $imageTEXT, $img ); break;
The PHP needs more error checking and all sorts of sanity tests, but I wanted to keep the
example simple.
To overcome the server memory requirements, you could simply append the incoming data
to a text file with file i/o, then run base64 –D (available on most Unix systems) to convert
the text file to binary. See man base64 for details. The EWB progress animation prevents
user input. If you do not use ShowProgress/HideProgress, you could put up a cancel button
which permits users to interrupt transfers at the end of the current segment by adding a
segment=-1 state which does not call the upload function, and which displays a ‘Cancelled’
message.
Uploads on many Internet connections are one fifth to one tenth the speed of downloads, so
uploading is often annoyingly slow for users. Your users will greatly appreciate the user
friendliness of the progress bar and a cancel button. Local file i/o is not limited to reading.
HTML5 has a wonderful file writing function which will someday make it trivial to write
files from JavaScript… once that function is implemented by the browser authors.
Until that time, you have the choice of writing a hugely complex subroutine which is
compatible with every browser by employing their multitude of strategies or enjoy the
benefits of the filesaver.js library which has done exactly that.
Here is an editor which lets you write/edit data and then save to a local file.
The source code is quite simple. Note, we have to specify the MIME type of the file.
unit firefoxsave1;
interface
uses WebDom, WebCore, WebUI, WebForms, WebCtrls, WebBtns,
WebBrwsr, WebComps, WebEdits;
type
TForm1 = class(TForm)
Button1: TButton;
Script1: TScript;
MultiLineEdit1: TMultiLineEdit;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
end.
You will need to define a TScript to load filesaver.js. Again, production code should check
that the script loads successfully.
The exact outcome of using filesaver.js depends on the browser. Current versions of
Chrome and IE and Edge display an indicator saying the file is downloaded and available
on the bottom of the window. FireFox displays a Save/Open popup box. Safari opens the
new text in a new browser window and it is up to the user to use File | Save.
You can do more than text. This next project downloads an Image from the Web, uses the
graphics routines to copy the image and place a red circle in the corner, then lets you save
the image to a local file. It uses filesaver.js and a library named canvastoblob.js which
takes a canvas and creates a PNG file from it suitable for saving locally. You will need
two TScripts, one for filesaver.js and one for canvastoblob.js.
Here I load a shopping cart image into a TImage. Then when you press ‘draw graphics’ the
app copies the image and adds a circle.
Press ‘save graphics’ to save the new image as a PNG file. Note, any area not drawn on by
our circle or the source picture will be saved as transparent. If this is a problem, first
cover the paint image with a background color.
TForm1 = class(TForm)
Script1: TScript;
btSaveGraphics: TButton;
Paint1: TPaint;
Image1: TImage;
Script2: TScript;
btDrawGraphics: TButton;
procedure btDrawGraphicsClick(Sender: TObject); procedure
btSaveGraphicsClick(Sender: TObject);
private
{ Private declarations }
savefilename : string;
procedure savefn( blob : object );
public
{ Public declarations }
end;
end;
end;
Paint1.clientID := 'mycanvas';
paint :=
THTMLElement(window.document.getElementById('mycanvas'));
context := THTMLCanvasElement( Paint ).getContext('2d');
eval( 'context.drawImage(image, 1,1);');
DrawCircleAt( Paint1, 25 ,25, 25 ); end;
procedure TForm1.savefn( blob : object ); begin
saveAs( blob, savefilename);
end;
procedure TForm1.btSaveGraphicsClick(Sender: TObject); var
paint : TCanvasToBLob;
begin
Paint1.clientID := 'mycanvas';
paint :=
TCanvasToBlob(window.document.getElementById('mycanvas'));
savefilename := 'test.png'; paint.toBlob( savefn ); end;
end.
If you are looking at this example to see how to copy an image into a TPaint, you will be
confused by:
image := THTMLElement(
window.document.getElementById('myimage'));
image := THTMLElement( image.childNodes[0] );
The thing is: EWB puts the image inside a DIV, and the clientID is assigned to the DIV, not
the IMG. So when we use getElementById we get the DIV but we want its child IMG. The
next line extracts the first (and only) child of the DIV, which is the IMG.
Smoothie is a scrolling chart JavaScript library. Use it to display real-time graphs of your
data.
Our example just plots two random numbers between 0 and 1000, using slight colorization
to make the charts interesting. In your real-world uses you could plot any real-time data.
For this project you will need a TTimer, a TPaint (which is really a JavaScript Canvas),
and a TScript to load the smoothie.all.js file.
unit smoothi1;
interface
uses Webdom, WebCore, WebUI, WebForms, WebCtrls, WebLabels,
WebComps, WebPaint;
type
TForm1 = class(TForm)
Label1: TLabel;
Script1: TScript;
Paint1: TPaint;
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject); procedure
Script1Load(Sender: TObject);
private
{ Private declarations }
smoothie : TGlobalSmoothieChart; line1, line2 :
TGlobalTimeSeries;
public
{ Public declarations }
end;
);
procedure addTimeSeries( s : TGlobalTimeSeries; options
: TGlobalSmoothieLineOptions );
end;
var
Form1: TForm1;
external function eval( s : string ) : object;
implementation
function LocalTime : integer; begin
result := integer(now); end;
end;
end;
procedure TForm1.Script1Load(Sender: TObject); var
options : TGlobalSmoothieLineOptions;
begin
Paint1.clientID := 'mycanvas';
smoothie := TGlobalSmoothieChart(
eval('new SmoothieChart();'));
smoothie.streamTo(window.document.getElementById(
'mycanvas'),1000);
// Add to SmoothieChart
options := TGlobalSmoothieLineOptions( eval('new
Object();')); options.strokeStyle := 'rgb( 0, 255, 0 )';
options.fillStyle := 'rgba( 0, 255, 0, 0.4 )';
options.lineWidth := 3;
smoothie.addTimeSeries(line1, options );
options.strokeStyle := 'rgb(255, 0, 255)';
options.fillStyle := 'rgba( 255, 0, 255, 0.3 )';
smoothie.addTimeSeries(line2, options );
end;
end.
The streamTo() call takes an options delay parameter we set to 1000. This is the number of
milliseconds to buffer before displaying data. Since we only provide new data every 1000
ms (TTimer’s default), the output would appear jerky if we did not buffer it for a full
second.
For the options, JavaScript’s rgb() returns an red green blue color-space value, whereas
rgba() returns the color-space with an opacity alpha, meaning the fill is semi-transparent.
Google Charts is an excellent way to add an interactive graph to your program. If the user
mouses over parts of the graph they will highlight and give various data information.
Pie, bar and other charts are easy once you see how to use the library.
Add a TScript which loads the library from Google. Set its onload to
You will need a DIV with ID set to chart_div to hold the graph. Simply place a
TBasicPanel on the form, it’s a DIV, and set its ID property with clientID := ‘chart_div’
before you use it.
For this example I used eval() more liberally to compute parameters for various functions.
Data is supposed to be an array of arrays by two, where the first element is a string and the
second is a number. We declare it as an array of array of variants, as variants are
compatible with strings and numbers. We must use SetLength() to set the length when
adding values.
The code is about 100 lines, and the results are very effective.
unit googlecharts1;
interface
uses WebDom, WebCore, WebUI, WebForms, WebCtrls, WebComps,
WebCtnrs, WebLabels;
type
TForm1 = class(TForm)
Script1: TScript;
BasicPanel1: TBasicPanel;
Label1: TLabel;
procedure Script1Load(Sender: TObject);
private
{ Private declarations }
options : TGVChartOptions ;
chart : TGChart;
data2 : array of Tdatum;
data : TGVis;
procedure drawChart;
public
{ Public declarations }
end;
TDatum = array of variant; TDrawProc = procedure of object;
external TGChart = class
public procedure draw( data : variant ; options : variant );
end;
end;
end;
end;
external TGoogle emit google = class public
property charts : TGCharts read write; end;
var
Form1: TForm1;
external google : TGoogle;
external function eval( s : string ) : object;
implementation
begin
// Create the data table.
data := TGvis( eval('new
google.visualization.DataTable()'));
data.addColumn('string', 'Topping');
data.addColumn('number', 'Slices');
setlength( data2, 5 );
data2[0] := [ variant('Mushrooms'), variant(3)]; data2[1] :=
[ variant('Onions'), variant(1)]; data2[2] := [
variant('Olives'), variant(1)]; data2[3] := [
variant('Zucchini'), variant(1)]; data2[4] := [
variant('Pepperoni'), variant(2)];
data.addRows( data2 );
chart.draw(data, options);
end;
end.
OAuth 2.0 is a protocol which allows social sharing. You can use it to find the identity of a
user from such social networks as Google, Microsoft, Facebook, Linked In, etc. You can
even use it to access friend lists, pictures, Google Drives, etc. Pinterest, for example, uses
it to determine your identity and your friends so it can notify them with what strikes your
fancy.
Unlike protocols of yore, OAuth 2.0 does not require your userid or password. Through a
complex handshake, it pops up a window which either asks for your credentials at the
social host (eg. Google), or it will find cached credentials from a previous time.
There are whole books on OAuth 2.0 and countless web sites explaining it We will use the
popular JavaScript library called Hello.JS which does most of the heavy lifting. A key
benefit of Hello.JS is that it makes it easy to support multiple vendor OAuth 2.0
implementations and hides the ugly differences.
<html><head>
<script src='hello.all.js'></script>
</head>
<body>
<button onclick="hello('google').login( function (){
token = hello('google').getAuthResponse.access_token;
});">Google</button>
<script>
var GOOGLE_CLIENT_ID =
'5770808996983asdfasdfasdfasdfasdfasdf.apps.googleusercontent.co
m' ;
hello.init(
{google : GOOGLE_CLIENT_ID },
{redirect_uri:'redirect.html',
scope: 'basic'}
);
hello.on('auth.login', function(auth) {
});
});
</script></body></html>
Note, you will need to get your own IDs from Facebook, Microsoft and Google for this
project. See the Hello.JS documentation to learn how to do this.
This simple program loads the Hello.JS library and adds a button which pops up an alert
with the URL to the user’s Google thumbnail picture.
In order to work, it also needs a redirect.html file that looks like this and is registered as
the redirect page for your ID:
<!DOCTYPE html>
<html>
<head>
<title>Hello, redirecting...</title>
<div
class="loading"><span>•</span><span>•</span>
<span>&bul l;</span></div>
We will reuse the redirect.html file exactly as the JavaScript example did, there is no
benefit to recoding it in EWB, and EWB is bigger and slower to load.
How can it be both a variable and a function? To be honest, it is more complicated than
most libraries to decipher.
A quick perusal of the hello.all.js source file indicates that hello( network )… is an
alias for hello.use( network )… So substituting that is our first step.
I built a sample application which displays the user’s name and thumbnail using EWB. It’s
a bit longer than the above JavaScript, but that’s because it does more, and it uses the
strong Object Pascal typing.
The source code is as follows:
unit test;
interface
uses Webdom, WebCore, WebUI, WebForms, WebCtrls, WebBtns,
WebComps, WebBrwsr, WebLabels;
type
TForm1 = class(TForm)
Script1: TScript;
btGoogle: TButton;
Image1: TImage;
Label1: TLabel;
Timer1: TTimer;
btFaceBook: TButton;
btWindows: TButton;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
procedure btGoogleClick(Sender: TObject);
procedure Script1Load(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure btFaceBookClick(Sender: TObject); procedure
btWindowsClick(Sender: TObject); private
{ Private declarations }
inited : integer; // when set to 1, timer enables the
buttons, then sets to 2
public
{ Public declarations }
end;
end;
end;
TProc = procedure;
TOnFn = procedure( auth : TAuth ); TThenFn = procedure( r :
TLoginUser );
// Hello.JS promise
external TApi = class
public
procedure then( r : TThenFn); end;
external TAuthResponse = class
public
property access_token : variant read; end;
external THello emit hello = class
public
property services : array of variant read write; function
use( vendor : string ) : THello; procedure init( args :
TGlobHelloArgs ; arg2 :
TGlobHelloRedirect );
procedure on( arg : string ; fn : TOnFn ); function api(
what : string ):TApi; procedure login( f: TOnFn );
function getAuthResponse : TAuthResponse;
end;
var
Form1: TForm1;
var
external hello : THello;
external procedure helloinit;
external procedure hellogo;
external function eval( s : string ): object;
myargs : TGlobHelloArgs;
myargs2 : TGlobHelloRedirect;
begin
myargs := TGlobHelloArgs( eval( 'new Object()')); myargs2 :=
TGlobHelloRedirect( eval( 'new Object();'));
myargs.facebook := '1490242342342845';
myargs.google :=
myargs.google :=
39nj9os1muja453trrp54dt50crthhv3.apps.googleusercontent.com'
; myargs.windows := '223423432f-07d8-234233-9940-
0233daf9ecd6';
myargs2.redirect_uri :=
'https://dark.uwaterloo.ca/oauth2/redirect.html';
myargs2.scope := 'basic';
form1.Label1.Caption := r.name;
form1.Image1.URL := r.thumbnail;
except
form1.Label1.Caption := 'error reading response'; end;
end;
inc( inited );
HelloEWBInit; // initialize the library btGoogle.Visible :=
True;
btFacebook.Visible := True; btWindows.Visible := True;
end;
end;
procedure TForm1.btGoogleClick(Sender: TObject); begin
hello.use('google').login(loginfn );
end;
procedure TForm1.btFaceBookClick(Sender: TObject); begin
hello.use('facebook').login(loginfn ); end;
procedure TForm1.btWindowsClick(Sender: TObject); begin
hello.use('windows').login(loginfn);
end;
end.
The buttons are initially set to invisible, so users do not click them before the library is
fully initialized.
Script1 loads hello.all.js, and calls the Script1 onload. I had it increment a variable, and
then that variable reaches the number of libraries we need to load (one in this case), timer1
enables the buttons. If you decide to load more libraries, repeat this step and enable the
buttons when all the libraries have loaded. There is a slight delay because Timer1 defaults
to one second intervals. The program is now pretty easy to read. Pressing a button calls the
appropriate vendor’s login with loginfn, and loginfn calls the ‘me’ function with loginfn2.
Loginfn2 updates the GUI. If case you are wondering, the reason we pass functions like
loginfn, loginfn2 to these functions is because they are called asynchronously after the
network calls complete.
OAuth2 is all about sharing. Having the client believe you are a certain identity is useful,
but often you will want to convince a server of your supposed identity. Enter the token.
OAuth2 returns a token which can be used to verify you identity and various other attribtues
about you.
unit test2;
interface
uses Webdom, WebHttp, WebCore, WebUI, WebForms, WebCtrls,
WebBtns, WebComps, WebBrwsr, WebLabels;
type
TForm1 = class(TForm)
Script1: TScript;
Script2: TScript;
btGoogle: TButton;
Image1: TImage;
Label1: TLabel;
Timer1: TTimer;
btFaceBook: TButton;
btWindows: TButton;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
procedure btGoogleClick(Sender: TObject); procedure
Script2Load(Sender: TObject); procedure Timer1Timer(Sender:
TObject); procedure btFaceBookClick(Sender: TObject);
procedure btWindowsClick(Sender: TObject);
private
{ Private declarations }
inited : integer;
procedure HelloEWBInit;
procedure RequestComplete( req : TServerRequest ); procedure
loginfn2( r : TLoginUser );
procedure loginfn( auth : TAuth );
public
{ Public declarations }
end;
end;
//promise
external TApi = class
public
public
property access_token : variant read;
end;
TGlobHelloRedirect );
procedure on( arg : string ; fn : TOnFn ); function api(
what : string ):TApi; procedure login( f: TOnFn );
function getAuthResponse : TAuthResponse;
end;
var
Form1: TForm1;
var
external hello : THello;
external procedure helloinit;
external procedure hellogo;
external function eval( s : string ): object;
implementation
procedure TForm1.RequestComplete( req : TServerRequest );
begin
ShowMessage( req.ResponseContent.Text ,'results');
HideProgress; end;
ServerRequest := TServerRequest.Create(Self);
with ServerRequest do begin
method := rmGet;
url :=
'assure.php?token='+token+'&brand='+network+'&name='+name;
onComplete := RequestComplete;
execute;
end;
ShowProgress('Waiting for local server to verify with
'+network );
end;
procedure TForm1.HelloEWBInit;
var
myargs : TGlobHelloArgs;
myargs2 : TGlobHelloRedirect;
begin
myargs := TGlobHelloArgs( eval( 'new Object()'));
myargs.facebook := '149024688853845';
myargs.facebook := '149024688853845';
39nj9os1muja453trrp54dt50crthhv3.apps.googleusercontent.com'
;
myargs.windows := '24d7e37f-07d8-4703-9940-0233daf9ecd6';
except
Label1.Caption := 'error reading response'; end;
end;
procedure TForm1.loginfn( auth : TAuth );
begin
Label1.Caption := 'reading....';
localnetwork := auth.network;
localtoken :=
hello.use(auth.network).getAuthResponse.access_token; try
hello.use( auth.network ).api('me').then( loginfn2 ); except
Label1.Caption := 'Your login failed.';
end;
end;
inited := 3;
HelloEWBInit;
btGoogle.Visible := True;
btFacebook.Visible := True; btWindows.Visible := True;
end;
end;
procedure TForm1.btGoogleClick(Sender: TObject); begin
hello.use('google').login(loginfn );
end;
procedure TForm1.btFaceBookClick(Sender: TObject);
hello.use('facebook').login(loginfn ); end;
procedure TForm1.btWindowsClick(Sender: TObject); begin
hello.use('windows').login(loginfn);
end;
end.
The major changes start where we grab the token and the network in the loginfn()
localnetwork := auth.network;
localtoken :=
hello.use(auth.network).getAuthResponse.access_token;
And when it’s done, RequestComplete() is called to cancel the progress message and
display the message from the PHP server.
end;
All we need now is the PHP code and some hope that everyone implements something
similar:
assure.php
<?php
// given a token, brand, check it is genuine
$server['facebook'] = "https://graph.facebook.com/me";
$server['github'] = "https://api.github.com/user";
$server['google'] = "https://www.googleapis.com/oauth2/v3/tokeninfo";
$server['windows'] = "https://apis.live.net/v5.0/me";
{
if ( $f = fopen("log","a")) { fwrite( $f, "$info\n"); fclose( $f );
}
}
?>
In any case, we take what we get and log it to a log file in the same subdirectory. On unix,
the file can be marked write-only so nobody can read it but those we intend.
Our OAuth adventure has been fun. There are surprisingly many things you can now do
with JavaScript, and most of them can be easily accomplished with JavaScript libraries.
Luckily EWB can interface to most JavaScript libraries with just a little effort. And you do
not need to wait for Elevate software to adopt your preferred JavaScript library, you can
usually interface to it yourself.
Most web browsers now run on multi-core CPUs, so it only makes sense that
multithreading would make its way to JavaScript, and thus to EWB through Web Workers.
Note: not all browsers support multithreading, and some do, but are still only co-operative
threading based.
The main page is the primary thread, it is the only thread that can access the GUI.
Other threads cannot:
There are no synchronization objects like in most other multithreaded environments;
instead it acts much like a network-connected service: you post messages to and from the
thread using postMessage() and receiving through onmessage().
Each thread is loaded from a JavaScript source file. It too can spawn additional threads
with some limitations. Read the Web Workers documentation for more details.
Here is a very simple HTML/JavaScript web page which loads and talks to a thread, and
can terminate the thread when needed:
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Web Worker Example</title>
<script type="text/javascript">
function QueryableWorker (sURL, fDefListener, fOnError) {
var oInstance = this, oWorker = new Worker(sURL);
};
this.sendQuery = function ( v ) {
oWorker.postMessage({ "fn" : 0 , 'value' : v });
};
this.postMessage = function (vMsg) {
oWorker.postMessage(vMsg);
};
this.terminate = function () {
oWorker.terminate();
};
};
// your custom "queryable" worker
var oMyTask = new QueryableWorker("worker1.js");
</script>
</head>
<body>
<ul>
<li><a id="firstLink" href="javascript:oMyTask.sendQuery( 0
This line loads and starts running our worker1 JavaScript file. Our example will simply
count seconds and send the second count to the main GUI every second.
oWorker.onmessage is called whenever the thread sends us data, and we display it in the
DIV we created with ID ‘answer’.
Whenever we want to send a message, in this case to reset our counter, we call sendQuery
which executes:
oWorker.postMessage({ "fn" : 0 , 'value' : v });
The function (fn) will be ignored in our example, but 0 is the value the second counter is
reset to.
We can terminate the thread at any time with oWorker.terminate().
Now we will write the worker1.js file in EWB Object Pascal. Create a NONVISUAL
project called worker1. The main source should look like this:
project worker1; contains workunit;
uses Webdom, WebCore;
begin
myOnMessageInit; // initialize the library
// log something to say we are started
console.log('started');
TMyEvent = class
public
fn : string ;
value : integer;
end;
TProc = procedure;
TProcEvent = procedure( e : TEvent );
external TConsole emit console = class public procedure log(
msg : string ); end;
external function eval( s : string ):object;
var
external console : TConsole;
external onmessage : variant; // we will fill this with our
own onmessage handler
external procedure postMessage( v : variant );
external procedure setTimeout( fn : TProc ; interval :
integer );
procedure doOne;
procedure myOnMessageInit;
procedure myOnMessage( e : TEvent );
implementation
procedure findPrimes; var
i : integer;
begin
i := 2;
repeat
var
count : integer = 0;
procedure doOne; var
my : TMyEvent; begin
inc( count ); console.log('uploading ' + IntToStr( count ));
my := TMyEvent.Create; my.value := count;
postMessage( my );
setTimeout(doOne, 500 ); end;
var eventcount :integer = 0;
procedure myOnMessage( e : TEvent );
var
my : TMyEvent;
declares the TEvent passed by the Web Workers library. All we care about is the data
property.
We will cast the TEvent.data to class
end;
It is TMyEvent which the two threads will pass back and forth. Fn refers to a function
name, so that they can specify different functions if you want. We won’t use fn in this
example, but it’s handy for more complex ones.
The myOnMessage procedure takes these incoming events and processes them. So if the
other side said the new value should be 100, we will blindly set our counter to that value.
We also log to the console, because otherwise debugging is terrible difficult with threads.
The doOne procedure increments our lucky counter, logs it to the console and posts the
updated counter to the main thread. It also schedules itself to run again in 500 milliseconds.
end;
The lines:
if False then
myOnMessage( nil ); // links it in but never executes
never actually run myOnMessage, but by writing this condition we tell the linker to copy
the myOnMessage() function into the output JavaScript. Otherwise the linker optimization
will skip it because it is not otherwise referenced by anything.
creates that global variable and sets it to our function. Note that the function name is
workunit_myonmessage(), which EWB creates because this unit name is workunit and the
function within the unit is myonmessage (no capitalization).
Compile your code and make one last critical change, edit the resulting worker1.js into an
editor and add the last line:
worker1_load();
Now load the master web page (main.html) and be amazed. The counter will increment
frequently, and you can reset the counter or kill the counter by clicking on the labels.
Consider the implications of what you have just seen. It is possible to write a program in
EWB Object Pascal and teach any JavaScript application to load and exchange data with
that program in real-time, let alone with multithreading with only a few additional lines of
JavaScript.
EWB is preferable to JavaScript for many of these threading situations because the
compiler should find most of your mistakes and you will spend less time debugging.
Create a Visual EWB project, we’ll call it mainewb, and a main form in a unit called
workmain.
unit workmain;
interface
uses WebDom, WebCore, WebUI, WebForms, WebCtrls, WebBtns,
WebLabels;
type
TForm1 = class(TForm)
btReset: TButton;
btTerminate: TButton;
Label1: TLabel;
procedure Form1Show(Sender: TObject); procedure
btTerminateClick(Sender: TObject); procedure
btResetClick(Sender: TObject);
private
{ Private declarations } worker : TQueryableWorker;
public
{ Public declarations }
end;
oInstance : variant;
procedure init;
procedure onMessageHandler( msg : TEvent ); procedure
postMessage( msg : TMyEvent ); procedure terminate;
end;
var
Form1: TForm1; external function eval( s : string ):object;
implementation
procedure TQueryableWorker.onMessageHandler( msg : TEvent );
begin
Form1.Label1.Caption := IntToStr(msg.data.value); end;
procedure TQueryableWorker.init;
begin
oWorker := TWorker( eval('new Worker("worker1.js");'));
oWorker.onmessage := onMessageHandler;
end;
begin
my := TMyEvent.Create;
my.fn := 'whatever';
my.value := 0;
worker.postmessage( my ); my.Free;
end;
end.
The only confusing things are that onMessageHandler() receives a TEvent which it must
convert into a TMyEvent, and the capitalization of onmessage is different from the
capitalization of postMessage… it’s just not consistent.
A very real use of threads is when you need to do scientific calculations or Internet
communications and want to offload them to other CPU cores to leave your GUI fast and
responsive. Again, not all browsers will do that, and some users still have single-core
machines out there. But the option exists for you to use, and as it is a standard, it is
becoming quite common. Check the web for latest compatibility news of Web Workers.
And EWB’s Object Pascal is perfect for doing complex calculations.
It is possible to write applications that will run natively on cell phones, tablets, set-top TV
boxes and other devices using EWB. Unike mere web pages, these applications will have
access to the built-in compass, GPS, gyrometer, camera, local file storage, etc.
What is spectacular is that native PhoneGap/Codova modules are available which can do
all sorts of things as add-in libraries. They are written in the language native to the device,
and they present a JavaScript-callable interface. As we saw earlier, if it’s accessible to
JavaScript: you can probably access it from EWB.
You have choices about how you convert these web pages into mobile apps: you can
download the free Cordova toolkit, and android toolkit, and Java toolkit, and IOS toolkit
(in which case you better be on a Mac), etc. Or you can spend $9.99 on an Adobe
PhoneGapBuild monthly subscription where they do all the work maintaining the libraries.
I’ve done it both ways in the past, using the PhoneGapBuild when I was lazy, and local
verions when I was cheap.
If you decide to do it locally, installation takes about an hour of downloading with a good
network connection. I won’t copy the instructions here because they seem to change every
month. Refer to the web for details. You’re probably wondering why I am discussing
distribution before even writing the app. Well, to test and share an Apple IOS app, you
have to get digital signatures from Apple. And everyone who tests your app has to have
their device entered into a list you maintain, up to 100 test users per app, no more. It’s all
quite complicated.
Testing Android apps is much easier. You will just generate an APK file, which is really
just a ZIP file, with a certain layout. Any android device can test it by downloading your
app from a web page.
mkdir cordova
cd cordova
cordova create hello
cd hello
cordova platform add android cordova platform add camera
Copy in your HTML and JavaScript files to the www subdirectory that you find.
cordova build android
You can copy almost any EWB project to the index.html its associated JavaScript file. In
fact, you can have an EWB project named: HappyDancer.html and HappyDancer.js. Just
rename the HTML file to index.html and keep HappyDancer.js, and it will work.
The project uses config.xml in the www subdirectory – cordova create hello put
it there. By default, the project uses the default icon. You can edit it and add your own
icons, etc. You really should edit the author field too. Read the whole file, but I recommend
keeping a backup in case you accidentally mess up the fields.
For our examples we will leave the config.xml file in its default state.
As we just discussed, you can place any html5/JavaScript code in index.* and it will make
an installable application. But what is really cool is that you can do more: you can access
hardware and software services on the device using Cordova/PhoneGap extensions.
Accessing those libraries and services is the challenge we shall solve.
Our first example will be a Camera will be able to take pictures and display them in an
TImage. You could also extend the program to upload images, keep inventory records, etc.
unit camera1;
interface
uses WebDom, WebCore, WebUI, WebForms, WebCtrls, WebComps,
WebBtns, WebBrwsr, WebSizer,
WebCtnrs;
type
TForm1 = class(TForm)
Script1: TScript;
Button1: TButton;
ScrollPanel1: TScrollPanel;
Image1: TImage;
procedure Script1Error(Sender: TObject);
procedure Form1Create(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Image1Load(Sender: TObject);
private
{ Private declarations }
procedure onDeviceReady;
procedure onCameraSuccess( s : string ); procedure
onCameraFail( s : string );
public
{ Public declarations }
end;
end;
external TNavigator2 = class (TNavigator) public property
camera : TCamera read write;
end;
var
Form1: TForm1;
external function eval( s:string) : object; implementation
TCordovaDocument(window.document).addEventListener(
'deviceready', onDeviceReady, false);
Width := window.Screen.Width;
Height := window.Screen.Height;
end;
begin
options := TCameraOptions(eval( 'new
Object()'));//TCameraOptions.Create;
options.quality := 20;
options.destinationType := 1; ///
Camera.DestinationType.FILE_URI;
TNavigator2(window.Navigator).camera.getPicture(onCameraSuccess,
onCameraFail, options );
end;
end.
You will need TScrollBox set with both scrollbars enabled, and its Layout set to cover the
form. Also you need a TScript we will explain in a second, a Button1 set to Visible:=
False and its caption set to ‘Take Photo’. And we need a TImage to hold the picture.
First, onCreate is called. It registers a callback which occurs sometime later when
PhoneGap is loaded and ready.
TCordovaDocument(window.document).addEventListener(
'deviceready', onDeviceReady, false); Width :=
window.Screen.Width;
Height := window.Screen.Height;
Then we have a TScript (not shown) which loads the URL cordova.js from the local
directory. Which cordova.js, there is none? At compile time, the Cordova system will
automatically place the correct JavaScript for this target OS in the local target
subdirectory.
Cordova fires an event called deviceready when it’s up and ready to do things. And
remember, we scheduled onDeviceReady to be called in onCreate. So it get’s called.
As you can see, it enables the user button to take a photo. Until now we had the button’s
Visible set to false so that they couldn’t take a picture before the system was ready.
Options is a class of options. We create the class (see chapter on JavaScript if it looks
strange). Then we call what JavaScript knows as
window.Navigator.camera.getPicture(). PhoneGap/Cordova has added a camera.getPicture
to the Navigator object. EWB is much more sensible than JavaScript-do-whatever-you-
want, so we must cast window.Navigator to a class we defined as Navigator2 above, and
TCamera
and
external TCamera = class
public property
end;
If the picture is taken, onCameraSuccess is called, and it copies the data url to the TImage
url property.
procedure TForm1.onCameraSuccess( s : string ); begin
Image1.url := s;
end;
Finally, when the image loads, we tell the system to expand the TImage to the size of the
Photograph by making space inside the TScrollPanel.
This creates a HelloWorld.app which can run under OS/X. Unfortunately, it does not
appear to support the camera, but at least it does not give errors. The screen looks like this
In all honesty, EWB added very little to this partiular project. It would have been faster to
code it in JavaScript. But where EWBl pays off is when you construct something more
heavy-duty, because that’s where EWB shines.
But first, let’s make this example more exciting. What good is taking an image until you can
upload it.
This example extends the previous one. It takes a picture then lets you upload the file if you
wish by pressing the Upload Button2.
Base64 encoding
• We use TServerRequest to upload the data with a POST
This builds on previous examples from other chapters.
What is not done in any previous chapters is that we set the host name of the URL. This
would break CORS on web pages, but it’s perfectly safe on an application.
unit camera1;
interface
uses WebDom, WebHTTP, WebCore, WebUI, WebForms, WebCtrls,
WebComps, WebBtns, WebBrwsr, WebSizer,
WebCtnrs, WebPaint;
type
TForm1 = class(TForm)
Script1: TScript;
Button1: TButton;
ScrollPanel1: TScrollPanel;
Image1: TImage;
Button2: TButton;
Script2: TScript;
Paint1: TPaint;
procedure Script1Error(Sender: TObject); procedure
Form1Create(Sender: TObject); procedure Button1Click(Sender:
TObject); procedure Image1Load(Sender: TObject); procedure
Button2Click(Sender: TObject);
private
{ Private declarations }
strimg : string;
HttpImageRequest : TServerRequest; procedure onDeviceReady;
procedure onCameraSuccess( s : string ); procedure
onCameraFail( s : string ); procedure UploadPicture;
procedure CopyPictureToImage;
end;
var
Form1: TForm1;
external function eval( s:string) : object;
implementation
TCordovaDocument(window.document).addEventListener(
'deviceready', onDeviceReady, false);
Width := window.Screen.Width;
Height := window.Screen.Height;
end;
begin
options := TCameraOptions(eval( 'new
Object()'));//TCameraOptions.Create;
options.quality := 20;
options.destinationType := 1; ///
Camera.DestinationType.FILE_URI;
TNavigator2(window.Navigator).camera.getPicture(
onCameraSuccess, onCameraFail, options ); end;
procedure TForm1.UploadComplete( sr : TServerRequest );
begin
if (sr.StatusCode <> HTTP_OK) then
const
host = 'https://erickengelke.com/temp'; theurl =
'/PostBase64PNG.php';
image := THTMLElement(
window.document.getElementById('myimage')); image :=
THTMLElement( image.childNodes[0] );
Paint1.clientID := 'mycanvas';
paint := THTMLElement(
window.document.getElementById('mycanvas')); context :=
THTMLCanvasElement( Paint ).getContext('2d'); eval(
'context.drawImage(image, 1,1);');
end;
procedure TForm1.UploadPicture;
var
s : string;
begin
// similar to the filesave example earlier in the book
CopyPictureToImage;
// s will be a Base64 version of the image
s := Paint1.Canvas.ConvertToDataUrl( 'image/png', 100 );
UploadPicture2( s );
end;
with HttpImageRequest do
begin
method := rmPost;
onComplete := UploadComplete;
RequestHeaders.Clear;
RequestContent.Clear;
'Content-Type: application/x-www-form-urlencoded');
RequestContent.add('imagedata=' + blob );
RequestHeaders.add('Content-Length: ' +
end;
procedure TForm1.Button2Click(Sender: TObject); begin
UploadPicture;
end;
end.
Of course you will need a script on the server to accept the uploaded image.
PostBase64PNG.php
<?php
$imageFilename = 'junk';
$img = $_POST['imagedata'];
file_put_contents( "$imageFilename.txt", $img );
This upload PHP script tries to save the file in the local subdirectory, you will want to
change that and also add code to ensure you trust this user to upload. The Map and GPS
example will create a working GPS system. It will get updates from the GPS every few
seconds and display our changing position on a Google Map. Elevate software has taken
the hassle out of Google Maps, and so this program is surprisingly short for what it
accomplishes.
unit geo1;
interface
uses Webdom, WebCore, WebUI, WebForms, WebCtrls, WebBtns,
WebLabels, WebMaps, WebComps;
type
TFormGeo = class(TForm)
Button1: TButton;
Label1: TLabel;
Label2: TLabel;
Map1: TMap;
Button2: TButton;
Script1: TScript;
Label3: TLabel;
procedure Button1Click(Sender: TObject); procedure
Button2Click(Sender: TObject); procedure
Script1Error(Sender: TObject); procedure
FormGeoCreate(Sender: TObject);
private
locallocation : integer;
watch : string;
procedure onSuccess( res : TGeoResults ); public procedure
onError( error : TGeoError ); procedure onDeviceReady;
end;
// which options can we supply to GPS?
external TGeoOptions = class
public
end;
onGeoSuccess : TGeoSuccessFn ;
onGeoError : TGeoErrorFn;
options : TGeoOptions ):string;
var
FormGeo: TFormGeo;
external function eval( s : string ):object;
implementation
procedure TFormGeo.Script1Error(Sender: TObject); begin
ShowMessage('cordova did not load','ERROR'); end;
procedure TFormGeo.onDeviceReady; begin
Button1.Visible := True;
end;
procedure TFormGeo.onSuccess( res : TGeoResults ); begin
Label1.Caption :=
'Latitude: ' + FloatToStr( res.coords.latitude );
Label2.Caption :=
'Longitude: ' + FloatToStr( res.coords.longitude );
Map1.Locations.Add;
with Map1.Locations[locallocation] do begin
Latitude := res.coords.latitude;
Longitude := res.coords.longitude;
showmarker := True;
end;
inc( locallocation );
end;
begin
options := TGeoOptions( eval('new Object();')); options.
maximumAge := 3000;
options.timeout := 5000;
options.enableHighAccuracy := true;
watch :=
TGeoNavigator(window.navigator).geolocation.watchPosition(
onSuccess, onError , options );
Button1.Enabled := False; Button2.Enabled := True; end;
procedure TFormGeo.Button2Click(Sender: TObject); begin
if watch <> '' then begin
TGeoNavigator(window.navigator).geolocation.clearWatch(
watch );
watch := '';
Button2.Enabled := False;
Button1.Enabled := True;
end;
end;
end.
The OnCreate event is almost identical to our last project, so it doesn’t need a explanation
except to say it disables the Disable GPS button.
When we get the deviceready, we enable the Start GPS button. So far so simple.
When the user clicks Button1, we prepare to call watchPosition() with two event handlers
and some options. This function tells the GPS to upcall our event handlers when changes
happen to the GPS position, or errors results such as when we are out of range of the
satellites. It will update the location usually every 3000 ms, or timeout after 5000ms of no
position recording.
Options.enableHighAccuracy means to use the GPS and not the cell towers location.
Higher accuracy results from this decision, but it also drains the batteries more quickly.
begin
options := TGeoOptions( eval('new Object();')); options.
maximumAge := 3000;
options.timeout := 5000;
options.enableHighAccuracy := true;
watch :=
TGeoNavigator(window.navigator).geolocation.watchPosition(
onSuccess, onError , options );
Button1.Enabled := False; Button2.Enabled := True; end;
The watchPosition returns a string token which we will use again when we want to turn off
the GPS.
procedure TFormGeo.Button2Click(Sender: TObject); begin
if watch <> '' then begin
TGeoNavigator(window.navigator).geolocation.clearWatch(
watch );
watch := '';
Button2.Enabled := False;
Button1.Enabled := True;
end;
end;
And an important part of this project is the onSuccess handler called by the GPS system:
procedure TFormGeo.onSuccess( res : TGeoResults ); begin
Label1.Caption :=
'Latitude: ' + FloatToStr( res.coords.latitude );
Label2.Caption :=
'Longitude: ' + FloatToStr( res.coords.longitude );
Map1.Locations.Add;
with Map1.Locations[locallocation] do begin Latitude :=
res.coords.latitude;
Longitude := res.coords.longitude;
showmarker := True; end;
inc( locallocation ); end;
It displays the latitude and longitude and adds a marker to the map. Markers are an indexed
array, so we need to inc( locallocation) to record the next location to a new marker.
You could use an upcall to Google services to convert the GPS coordinates to an
approximate street location. My experience is that it’s usually accurate to the street name
and approximate house, but not necessarily the exact house.
Note that Google starts charging for or blocking access to location awareness nformation
when you use it too much. So be careful to limit requests to a reasonable number per day
where reasonable is defined by Google.
Look at the Apache Cordova web site and you will find extensions that do all sorts of
things. They can make your program
run in the background
accept asynchronous notifications like facebook does
access files
access an SQL-lite based database system
and much more
All the libraries use the same sort of calls as we did in our two examples, so there is no
excuse for not persuing these options.
The next steps are generally to register for Android, IOS and other developer keys and
signatures. This will cost some money but it is the frustration with endless web pages
which will probably be the hardest part. Also, Apple is picky about what it will allow on
its store, you need to meet their user interface guidelines and various other criteria.
Not every developer will feel a need to make a mobile app, but with EWB it is relatively
easy to accomplish, and definitely no harder than competing environments.
even more)
• ability to use Microsoft embedded / IIS server
• connectivity to every major brand of SQL server and NOSQL server
Personal Aside
It was while researching my unfinished Mormot book that I got sidetracked by discovering
EWB, eventually leading me to write this book. My first Mormot book will likely be my
next.
This short chapter will not introduce Mormot in detail. It’s an environment much more
complex than EWB. But we will create three examples which work with programs that
ship as Mormot’s own server samples.
Mormot can be used as an Object Relational Mapping or ORM, which means you do not
have to write a line of SQL, and using it this way offers some benefits. But if you want to
write SQL, that’s fine too, you just bypass the ORM layer.
The following chapter describes my EWBMormot interface library and applications that
use it.
The first step in your EWB web page unit is to include both EWBmormot and WebHTTP.
Then there are two important classes for all Mormot EWB applications:
TMormotConnection class which communicates with the Database/Web server
TMormotTable class from which classes you create will descend.
In all Momot applications on all platforms you utilize a class that knows how to
communicate with the database/web server: TMormotConnection, somewhat akin to the
TDatabase of EWB.
Also, you define data objects as classes that descend from an ancestor Mormotrelated class
which knows how to express them to the aforementioned connection class.
For our first two examples, we will use the Project 18 database server that comes with
Mormot. Project18 is a password-free database that lets you create, read, update and
delete Fish names.
In all EWBMormot applications you start by defining the class that describes the database
object you will connect to.
In our case, it’s TFish that describes Fish.
Noticed that it descends from TMormotTable rather than TPersistent. Otherwise it should
be quite familiar.
Next, in our form we define instances of the Web/Database and a Fish.
private
{ Private declarations } fish : TFish;
mormot : TMormotConnection;
unit sample2a;
// Demonstrates typical Mormot style of CRUD on tables
interface
TForm1 = class(TForm)
MultiLineEdit1: TMultiLineEdit;
procedure Form1Show(Sender: TObject);
private
{ Private declarations } fish : TFish;
mormot : TMormotConnection;
procedure PostAddFish( sr : TServerRequest;
method : TRequestMethod ;
statuscode : integer; rh : TStrings; Response : TStrings );
var
Form1: TForm1;
implementation
//
MultiLineEdit1.Lines.Add('Fish ['+ IntToStr(Fish.ID)+'] = '
+ Fish.Name );
end;
procedure TForm1.Form1Show(Sender: TObject);
begin
//
mormot := TMormotConnection.Create;
Mormot.InitServer(
'http://127.0.0.1:8080/root/SampleRecord',
self);
fish := TFish.Create; fish.Name := 'Bobby';
MultiLineEdit1.Lines.Add('Default ID for fish is ' +
IntToStr( fish.Id ));
Our next example merges Mormot with TDataSet, so you can use EWB DataSet-enabled
controls with Mormot-proven database backends.
Specifically, we will use TGrid to display a number of fish in a master-detail view, with
TEdits and other controls showing the details.
For this example we need a screen layout that looks like this:
It also has a invisible TDataSet1 which is connected to the edit boxes, the date box, and the
TGrid.
I used Layout to make the TGrid shrink or swell to the size of available space. You can
learn more about that in our chapter on Responsive Design. The other controls were not
made responsive in this simple example.
Instead of Get, we call GetAllData, which downloads the data into our DataSet1 and it
flows beautifully into our table.
But to get it into the dataset, we have to implement a very short OnPopulate helper:
Much of the rest of the code couldn’t be simpler. For example, when a user pressed
btDelete, our code reads the ID number from the TDataSet cursor and deletes it.
The Create and Update functions are both wrapped into the Save button. First, pressing
Create or Update will set a flag to indicate if we are creating new or updating existing
data, that flag is stored in the insert Boolean.
fish.LoadFromDataset( DataSet1 );
if mormot.Inserting then
Mormot.Post(fish )
else
end
fish.LoadFromDataSet(..) loads the TFish structure with the values in the current dataset.
If it’s new, we Post( fish), which is the RESTful way to send a new object, otherwise we
Put( fish , and it’s ID ) to update the existing object.
The entire program is about 120 lines, including 65 lines of Interface.
unit sample1a;
interface
uses WebHTTP, WebCore, WebUI, WebForms, WebCtrls, WebCtnrs,
WebGrids, WebLabels, WebData,
WebBtns, WebEdits, ewbmormot;
type
TForm1 = class(TForm)
BasicPanel1: TBasicPanel; BasicPanel2: TBasicPanel; Label1:
TLabel;
Grid1: TGrid;
DataSet1: TDataSet;
GridColumn1: TGridColumn; GridColumn2: TGridColumn;
GridColumn3: TGridColumn; GridColumn4: TGridColumn; Label2:
TLabel;
Label3: TLabel;
Label4: TLabel;
Edit1: TEdit;
DateEditComboBox1: TDateEditComboBox; Edit2: TEdit;
btCreate: TButton;
btUpdate: TButton;
btDelete: TButton;
btRefresh: TButton;
Label5: TLabel;
btSave: TButton;
Label6: TLabel;
procedure DataSet1Show(Sender: TObject); procedure
btCreateClick(Sender: TObject); procedure
btUpdateClick(Sender: TObject); procedure
btDeleteClick(Sender: TObject); procedure
btRefreshClick(Sender: TObject); procedure
btSaveClick(Sender: TObject);
private
{ Private declarations }
fish : TFish;
mormot : TMormotConnection;
public
{ Public declarations }
procedure OnPopulate( s : string );
end;
var
Form1: TForm1;
fish := TFish.Create;
mormot := TMormotConnection.Create;
Mormot.InitServer(
'http://127.0.0.1:8080/root/SampleRecord', self);
Mormot.Populate := OnPopulate;
Mormot.GetAllData( fish );
end;
fish.LoadFromDataset( DataSet1 );
if mormot.Inserting then
Mormot.Post(fish )
else
end;
end.
The two preceding examples demonstrate Mormot Server’s REST API. Mormot also
supports interfaces for remote procedure calls (RPC). With the RPC you can send data to
the server, implement any server functionality, and return results that may but need not be
database results.
RPCs are great for implementing business logic, so the logic is defined and carefully
controlled by the server and not the possibly hacked web client.
For our simple example we define an RPC that just adds two numbers and wish to execute
it on the server for the client. The client gets an upcall when the results are done, and it
displays the results graphically.
Mormot’s own example 14 includes a server which does exactly that. It accepts two
numbers, parameters: num1 and num2, and adds them to return a result.
However, unlike the previous examples, it requires a login – which is probably more akin
to what you will encounter in a typical non-public database. All your production code
should use HTTPS rather than HTTP. But is that enough to make you safe? Certainly not.
HTTPS traffic is not immune to manin-the-middle (MITM) attacks. In fact, even a typical
user can press F12 on most browsers to see your passwords in clear text.
In addition to HTTPS, Mormot uses an excellent api where the client and server verify
who each other is, thereby preventing a man-in-the-middle attack by: never transmitting a
clear-text password, keeping it a secret
always putting a timestamp on requests to prevent attacker replays
digitally signing every message from client to server
So the client and the server keep a hash of the shared password, but they never
communicate it to each other. Instead, the server presents a challenge to the client saying
“use this challenge number in a mathematical formula with the secret hash”, to which there
is exactly one answer in 4 billion random attempts. The client can easily solve the problem
if he knows the hash, but can only guess if he does not. If the client just guesses, the server
knows and can take action to thwart further hacking attempts.
Our interface library does all that work for you. You just supply the userid/password pair
with a call to Authenticate.
For this sample program we have two edit boxes for the two numbers to add, two more
edits for the userid and password, two buttons (login and calculate) and a few labels for
the + plus sign and the answer. We also need a TScript pointing to the sha256.js JavaScript
because at the time of writing I have not yet converted sha256.js to EWB.
The whole EWB program is here:
unit sample3a;
interface
uses WebHTTP, EwbMormot, WebCore, WebUI, WebForms, WebCtrls,
WebEdits, WebLabels, WebBtns,
WebComps;
type
TForm1 = class(TForm)
Edit1: TEdit;
Label1: TLabel;
Edit2: TEdit;
Label2: TLabel;
Button1: TButton;
edUserName: TEdit;
edPassword: TEdit;
Button2: TButton;
Script1: TScript;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Form1Show(Sender: TObject);
private
{ Private declarations }
mormot : TMormotConnection; contract : string;
procedure sum( num1, num2 : string ; proc : TFnOnSum );
procedure OnSum( s : string );
public
{ Public declarations }
end;
published
property n1 : integer read fn1 write fn1; property n2 :
integer read fn2 write fn2;
end;
s : string;
i, j : integer;
begin
if statuscode < 300 then begin
s := Response.Text; // eg. {result: [5]}
i := Pos('[',s ); // find first
j := Pos(']',s, -1 ); // search backwards from end s :=
Copy( s, i+1, j-i-1 );
s := 'http://localhost:888/root/Calculator.Add';
mormot.PostOp := PostSumOp;
mormot.RawPost( s, sum1, '') ; // or call RawPost(s, Nil, '[
'+num1+', '+num2+']' );
sum1.Free;
end;
begin
mormot := TMormotConnection.Create;
Mormot.InitServer(
'http://127.0.0.1:8080/root/SampleRecord',
self);
mormot.OnLogin := OnMyLogin;
mormot.Authenticate( hostport , edUserName.Text,
edPassword.Text );
end;
procedure TForm1.Form1Show(Sender: TObject);
begin
end.
The first step is Form1Show which disables the Calculate button until the user is logged
into the system.
Button1.Enabled := False; // disable Calculate button until
logged in
Ensure your application always makes it clear that a user must login before accessing the
server.
Button2 is the login button.
var
hostport : string = 'http://localhost:888';
begin
mormot := TMormotConnection.Create;
Mormot.InitServer(
'http://127.0.0.1:8080/root/SampleRecord', self);
mormot.OnLogin := OnMyLogin;
mormot.Authenticate( hostport , edUserName.Text,
edPassword.Text );
end;
The login button sets an OnLogin pointer to our OnMyLogin function to change the user
interface when we are logged in. Then it stores and processes the login
connection/userid/password.
Within a fraction of a second Mormot will reply and, if your password is correct, you will
get an upcall to OnMyLogin.
In addition to showing the user we are logged in, and re-enabling the Calculate button, it
calls sum_contract to verify the sum procedure works. Mormot replies with a contract
number. On a more advanced implementation, this would do something clever, but ours just
discards this contract number.
Now the user is free to enter two numbers and press Button1 which is the calculate button.
procedure TForm1.Button1Click(Sender: TObject); var
num1, num2 : string;
begin
num1 := Edit1.Text; num2 := Edit2.Text;
Label2.Caption := ' = calculating...';
sum( num1, num2, OnSum ); end;
Button1Click converts the text entries into number and calls sum() which is a local function
call… but it calls a remote function call to do the work.
s := 'http://localhost:888/root/Calculator.Add';
mormot.PostOp := PostSumOp;
mormot.RawPost( s, sum1, '') ; // or call RawPost(s, Nil, '[
'+num1+', '+num2+']' );
sum1.Free;
end;
There are a variety of ways you can transfer the numbers to the sum() RPC. The easiest is
what we’ve shown, to declare a structure and populate it with the parameters needed. In
our case, the reference document said the two parameters are num1 and num2. If you use
names, as we have, you do not need to worry about ordering.
PostSumOp is called by both the contract setup sequence and by the sum() function. But we
know the difference between the two because at contract time the contract number is ‘’,
whereas at sum() time it will have already been set by our previous call.
s : string;
i, j : integer;
if statuscode < 300 then begin
PostSumOp is passed something a 200 series result code and a string like { result : [ 5] }.
Rather than make a complicated result parsing system, I just remove the
surrounding [ ]’s and process the middle text.
If this is our first call, we know it’s a contract number (which we just note but do nothing),
and on subsequent results we know we have the result of the remote Sum() function, so we
update the label saying the resut.
If the StatusCode is 300 or higher, there is an error of some sort, so we display it.
There are a large number of samples that ship with Mormot, and with the three examples
given here demonstrate four important components of Mormot development:
This is my fourth book and it was a pleasure to research and write. I think I’m getting better
with each book, but that’s just my take. I know the project has taught me a lot. And I find the
experience very intimate with the reader, trying to anticipate your questions all while trying
to remain short and concise.
I learned long ago that elegance means beautifully simple, though in English we often
confuse it with ornate which is actually the opposite. The simpler your code is, the less
likely it is to confuse you and contain errors. My code is usually simple, and I also try to
exend that to my writing style; you are the judge of whether that has been successful.
Your feedback will help make the book better. If there was something I glossed over or
missed entirely or maybe got wrong, please do let me know so I can improve the
experience for future readers.
One of the benefits of modern publishing is small batches and on-demand printing, so we
can make new revisions without a large write-off of inventory. For the consumer this means
authors are encouraged to update their material.
I also offer consulting services if you have a difficult problem and need assistance.
Finally, I wish you the best of luck with your future EWB projects. Maybe our paths will
cross again.
Erick Engelke
erickengelke@gmail.com http://erickengelke.com
Alpha Arc
ArcTo
asSineEaseIn
asSineEaseInOut asSineEaseOut asSineEaseOutIn
asBackEaseIn
asBackEaseInOut asBackEaseOut
asBackEaseOutIn asBounceEaseIn asBounceEaseInOut asBounceEaseOut asBounceEaseOutIn asCircEaseIn
asCircEaseInOut asCircEaseOut
asCircEaseOutIn asCubicEaseIn
asCubicEaseInOut asCubicEaseOut asCubicEaseOutIn asElasticEaseIn
asElasticEaseInOut asElasticEaseOut asElasticEaseOutIn asExpoEaseIn
asExpoEaseInOut asExpoEaseOut
asExpoEaseOutIn asLinear
asNone
asQuadEaseIn
asQuadEaseInOut asQuadEaseOut
asQuadEaseOutIn asQuartEaseIn
asQuartEaseInOut asQuartEaseOut
asQuartEaseOutIn asQuintEaseIn
asQuintEaseInOut asQuintEaseOut
asQuintEaseOutIn
BeginPath
BezierCurveTo
ClearRect
Clip
ClosePath
CompositeOperation
ConvertToDataURL FillColor
FillGradient FillPattern
FillStyle
LineCapStyle LineJoinStyle
LineWidth
ShadowBlur
ShadowColor ShadowOffsetX ShadowOffsetY
M iterLimit
TextAlign
TextBaseLine