Escolar Documentos
Profissional Documentos
Cultura Documentos
R E L A T E D L A N G U A G E S / A N D R O I D,
I O S, M A C , W I N D O W S & L I N U X
P R I N T E D, P D F, & O N L I N E V I E W
DX
CONTENTS
ARTICLES:
Harvard researchers developed a model to design a soft robot that bends like an index
finger and twists like a thumb when powered by a single pressure source.
Credit: Harvard SEAS.
Designing a soft robot to move organically -- to bend like a finger or twist like a wrist -
- has always been a process of trial and error. Now, researchers from the Harvard John
A. Paulson School of Engineering and Applied Sciences and the Wyss Institute for
Biologically Inspired Engineering have developed a method to automatically design
soft actuators based on the desired movement.
ADVERTISERS
BARNSTEN PAGE 5
BLAISE BOOKSHOP PAGE 11
DELPHI & PASCAL EVENTS PAGE 2
DEVELOPERS EXPERTS PAGE 22
GNOSTICE PAGE 12
COMPONENTS4DEVELOPERS PAGE 44
Wim Van Ingen Schenau -Editor Peter van der Sman Rik Smit
wisone @ xs4all.nl sman @ prisman.nl rik @ blaisepascal.eu
www.romplesoft.de
Editor - in - chief
Detlef D. Overbeek, Netherlands Tel.: +31 (0)30 890.66.44 / Mobile: +31 (0)6 21.23.62.68
News and Press Releases email only to editor@blaisepascal.eu
Editors
Peter Bijlsma, W. (Wim) van Ingen Schenau, Rik Smit,
Correctors
Howard Page-Clark, James D. Duff
Trademarks
All trademarks used are acknowledged as the property of their respective owners.
Caveat Whilst we endeavour to ensure that what is published in the magazine is correct, we cannot accept responsibility for any errors or omissions.
If you notice something which may be incorrect, please contact the Editor and we will publish a correction where relevant.
Subscriptions ( 2013 prices )
1: Printed version: subscription € 80.-- Incl. VAT 6 % (including code, programs and printed magazine,
10 issues per year excluding postage).
2: Electronic - non printed subscription € 50.-- Incl. VAT 21% (including code, programs and download magazine)
Subscriptions can be taken out online at www.blaisepascal.eu or by written order, or by sending an email to office@blaisepascal.eu
Subscriptions can start at any date. All issues published in the calendar year of the subscription will be sent as well.
Subscriptions run 365 days. Subscriptions will not be prolonged without notice. Receipt of payment will be sent by email.
Subscriptions can be paid by sending the payment to:
ABN AMRO Bank Account no. 44 19 60 863 or by credit card: Paypal
Name: Pro Pascal Foundation-Foundation for Supporting the Pascal Programming Language (Stichting Ondersteuning Programeertaal Pascal)
IBAN: NL82 ABNA 0441960863 BIC ABNANL2A VAT no.: 81 42 54 147 (Stichting Programmeertaal Pascal)
Subscription department Edelstenenbaan 21 / 3402 XA IJsselstein, The Netherlands / Tel.: + 31 (0) 30 890.66.44 / Mobile: + 31 (0) 6 21.23.62.68
office@blaisepascal.eu
Copyright notice
All material published in Blaise Pascal is copyright © SOPP Stichting Ondersteuning Programeertaal Pascal unless otherwise noted and may
not be copied, distributed or republished without written permission. Authors agree that code associated with their articles will be made
available to subscribers after publication by placing it on the website of the PGG for download, and that articles and code will be placed on
distributable data storage media. Use of program listings by subscribers for research and study purposes is allowed, but not for commercial
purposes. Commercial use of program listings and code is prohibited without the written permission of the author.
● Offer #1:
15% discount on the Architect Version
+ a FREE RAD Server Site license (value € 5.000,--!)
+ 15 months technical support and all new versions
● Offer #2:
10% discount on the Enterprise version
+ FREE RAD Server 10 user/device license
+ 14 months technical support and all new versions
● FREE Bonus Pack (value € 899,--) with every 10.1 Berlin purchase
and also available for current 10.1 Berlin customers with an
active subscription.
The Bonus Pack contains Konopka Signature VCL Controls, Radiant Shapes,
Marco Cantù's Object Pascal Handbook and VCL & FMX premium styles.
It is even more easy and fast than ever to update your VCL applications and
sell them to customers all over the world via the Windows Store.
5
Issue 5
Nr 1 2017 BLAISE PASCAL MAGAZINEIssue Nr 1 2017 BLAISE PASCAL MAGAZINE
MVVM DELPHI APPLICATIONS PAGE 1/5
EX PERTS
BY OLAF MONIEN
starter expert DX
Delphi & Lazarus ADVANTAGES
MVVM is a design pattern that has been This proposed design of MVVM promises a separation
of functional units (or concerns), so that the
created to help developing applications that
developer(s) can easily understand, track and alter
separate UI from business logic.
the application's behavior. It also makes it easier to
Applying this pattern to Delphi applications exchange the view with another one, without
without breaking all the comfort that Delphi is breaking the whole application – be it for multi-
renowned for is the challenge that we will take device development, reusability or for testability.
in this article. In short, by providing a stricter Separation of
Concerns (SoC) MVVM promises:
Even though the MVVM pattern has gotten a lot of
attention during the past couple of years, it is actually ● better maintainability
already about 12 years old. The pattern was first ● code that is easier to test
published in 2005 by John Gossman, a Microsoft MVP , ● better reusability
based on the works of Ken Cooper and Ted Peters, ● better support for multi-device development.
employees of Microsoft's. The pattern was created with
Microsoft's WPF/Silverlight platform in mind, but as a The latter has become more and more important,
pattern, it is not really tied to .NET or even C# or since mobile device development got into our focus.
VB.net. It can basically be applied to any UI In other words: Especially as a FireMonkey developer
application , written in any programming language. (a.k.a. multi device developer), we should check out
Not just as Delphi developer it is important to the MVVM pattern, to see how it could improve our
understand the concepts and goals of MVVM, so let's development process. Of course, even if you are
have a look at the theory, before we start coding. “just” a VCL developer , you would still be interested
to make your applications better maintainable and
Note: testable. As a side effect of strictly separating the
I'll use the term “Delphi” quite frequently in this article, View, it is basically possible to have the UI designed
but the techniques presented here apply to Lazarus / by “non-programmers”, i.e. by UI specialists.
FreePascal accordingly. The code examples will be for Delphi,
comments and feedback from Lazarus/FreePascal users is of In practice UI designers still need to run Delphi and
course welcome. probably need to understand certain Delphi basics,
but technically it would be possible to let these UI
The core idea of MVVM is to separate the View from the specialists work with just Delphi starter editions, as
Model and use a data binding mechanism to keep the long all visual components are available.
coupling between those two blocks as loose as possible.
MVVM consists of four components: DISADVANTAGES
In theory, the databinding mechanism may lead to a
1. Model higher memory and CPU usage footprint. In practice
2. View both, memory and CPU are not a big issue – even on
3. ViewModel mobile devices, as long as the application only creates
4. Data binding mechanism (Binder) and runs those views that are really needed at the
moment.
As depicted in , the ViewModel sits in-between the View Another criticism is, that the overhead for simple
and the Model. The acronym “MVVM” has been built applications is too high, and for complex applications
top-down though (as above), which may look unrelated it might become more difficult applying the pattern.
when you first get in contact with it. Both is more a matter of experience though.
Once you get used to the
pattern and use the right
tools, even small
applications will profit
from it. Larger applications
still need enough time to be
designed properly and the
pure number of units may
overwhelm the application
architect, but effectively,
applying “the old legacy
concepts” in such an
application is fast and easy
(because we are used to it)
but may lead to chaos and
unmaintainable code
easily.
MULTI-DEVICE SUPPORT
As neither the Model nor the ViewModel know anything
about the View(s), and because both, presentation and
business logic is kept away from the View(s), it
becomes a realistic task to exchange a View with
another. No logic that has to be copied from one View to
the other. Just hook a View to its ViewModel and start
designing.
As a side effect, those who are working on the logic
code rarely need to touch any of the Views units, which
makes it easier to let UI specialists work on those units
exclusively.
DELPHI
How does all this theory fit into Delphi? Lets start by
looking at a typical Delphi legacy application:
Our application “OrderSystem” only has one form
(TFormMain) and a data module (TDMMain). Figure 2 : Legacy App - MainForm
The form displays a list of orders and shows some
details for the currently selected order. There is also a
button “Order shipped” , which tells the system that the
currently selected order has been handed over to the
parcel service. Once an order is shipped, the button
should be disabled and the details of an order become
readonly.
To keep things simple, TDMMain contains a
TClientDataset (QOrders) that has some order
sample data assigned in the IDE. Thus, we do not need Figure 3: Legacy App - Data Module
to go into the details of DB configuration.
As “OrderSystem” uses a TDatamodule, it is not a
totally dumb application, that puts everything, controls
and data access components (queries that is), on the
form(s). Yet, it is still a legacy application.
Even though this legacy app separates out the data ViewModel
from the form and correctly connects the form through The ViewModel sits between the View and the Model. In
a TDatasource component to the data module it MVVM sample originating from .NET/C# this is usually
clearly mixes business and presentation logic into the a plain unit. We could of course use a plain unit in
user interface units: Delphi as well, but that would take away some power
There is basically no code in the data module (just some from Delphi in my opinion.
lines to simulate data persistence by reading/writing the So let's create a new data module and name it
TClientdataset's content from an XML file), all the “Main.ViewModel.pas” / “MainViewModel”.
business logic is in a ButtonShippedClick event To connect the ViewModel with the Model, use
handler. All the presentation logic code is in the two Main.Model in it, then drop a TDatasource onto it
remaining UI event handler methods. and connect it to ModelMain.QOrders. Next use
The logic code directly accesses or modifies the data in Main.ViewModel in Main.View, to provide a
the data module. connection from the view into its ViewModel.
This is a very trivial application, but clearly – even • Main.View uses Main.ViewModel
though the developer used a data module for • Main.ViewModel uses Main.Model
separation – the form is tightly bound to the data • Main.ViewModel uses a TDatasource to
module. This results in the expected issues: connect to Main.Model's data
● Hard to test Next, connect the DBGrid and the DBEdit in the View
● No easy exchange of the UI with the TDatasource in the ViewModel. The
● Logic developers have to spent Button's OnClick event in the View should look
most of their time in form units like that:
procedure TViewMain.ButtonShippedClick(
Sender: TObject);
FROM LEGACY TO MVVM begin
Let's identify MVVM components in Delphi. ViewModelMain.OrderShipped;
View end;
A View is basically a TForm in Delphi, consisting of
its DFM/XFM and PAS files. Our FormMain is the In the ViewModel, OrderShipped just gets
only view of our Orders System. routed through to the model:
To make it more clear what units relate to what procedure TViewModelMain.OrderShipped;
component in terms of MVVM, I tend to use a begin
prefixing mechanism: ModelMain.OrderShipped;
FormMainU.pas becomes “Main.View.pas”, end;
any additional form in our application would always
start with “View.”. The form itself is named The actual Business logic is then
“ViewMain”. performed in the Model:
Why not name the unit “View.Main.pas”
instead? Well, during development, you often look procedure TModelMain.OrderShipped;
for pieces that belong together: Main.View, begin
Main.ViewModel, Main.Model – with files QOrders.Edit;
named like that it is much easier in my opinion to QOrdersShipDate.Value := now;
navigate through the project manager. QOrders.Post;
Why not name the form itself “MainView” then end;
(periods are not allowed in component names)?
Next, we need an OrderIsShipped function
Well, while typing code, when you need a reference to
of type Boolean in the ViewModel,
let's say a View, then you just start typing “View…” and
then Delphi's code completion jumps in and offers
which is used from the View:
everything that starts with “View…”, which makes function TViewModelMain.OrderIsShipped: boolean;
typing very easy – at least for me. begin
result := not ModelMain.QOrdersShipDate.IsNull;
Model end;
A Model is the class or unit that holds business logic
procedure
and provides access to the database. In Delphi terms, a TViewMain.GridOrdersDrawColumnCell(Sender:
data module can serve as model. There may also be TObject; const Rect: TRect; DataCol: Integer; Column:
business classes such as TOrder, TCustomer, TColumn; State: TGridDrawState);
which would also be considered models. I will cover begin
this in a later article. // Gray backround for all already shipped orders
DMMainU.pas becomes “Main.Model.pas”, the if ViewModelMain.OrderIsShipped then
data module itself is called “MainModel”. GridOrders.Canvas.Brush.Color := clSilver;
The Model will need a method OrderShipped, GridOrders.DefaultDrawColumnCell(Rect, DataCol,
which will be called from the ViewModel. Column, State);
end;
CONCLUSION
In the demo above we created a Delphi MVVM application that follows the concepts of MVVM
without breaking the comfort that Delphi provides as a RAD tool. We only have seen a very
simple application, which serves well as a starter though. It clearly demonstrates:
● The View only contains code to “paint” controls, depending on states in its ViewModel
● The ViewModel is strictly separated from the View
● The ViewModel only contains presentation properties and presentaion logic
● The Model is strictly separated from the ViewModel and View
● The Model only contains business logic and data access.
In the next article, I will introduce some more advanced topics such as how to use business
classes like TCustomer and Torder.
There is also an alternative of how Order data change notification can be sent:
Currently, just one receiver (the Main View in the demo) can be notified of order data changes,
by registering an OrderDataChange method. We could certainly implement some sort of
container, that would be able to handle more than just one registration, but this might become
more complex than expected.
The alternative is to use a “global message center” that distributes notifications from multiple
senders to multiple receivers. This is a technique, that I will present in the next article as well.
AVID
l
es.htm
Gam
DAVID DIRKSE COMPUTER MATH & GAMES IN PASCAL
ath_
puterM
D ales
at
pres laisepas
.b
cal.e
u/Da
vidD
irkse
/Com
INCLUDING:
www
• 53 projects and code fully explained.
HOWARD PAGE-CLARK
end;
end;
L I B R A R Y 2 0 1 6
TH & 3 4
1 2 5
MA
UTER PASCAL
6 10 12
7 8 9 11
M P 13 18 19
CO S IN 14 15 16 17 27
ME 20 25 26
GA 28
21
22 23 24
34
35 36 37
29 30 31+ 33
32
32 40 41 42
38 39 45+ 43
POCKET EDITION 46 47 48 49
44
Also printed in full color. 50 53
51 52 54+
A fully indexed PDF file is included. 55
Resize, rotate, compress digital images.
Design your own font, generate and reduce
Truth Tables from Boolean algebra.
And more important: understand how it all
works!
The book contains 87 chapters, 53 projects
with source code and compiled programs
(exe). The book is highly educational and
suitable for beginners as well as for WINDOWS 10
Overview
Gnostice XtremeDocumentStudio Delphi is Use a single viewer control to Export reports from ACE
the next-generation multi-format display multiple formats - Reporter, FastReport,
document-processing component suite for documents (DOCX and PDF) QuickReport and
Delphi/C++Builder developers. and images (BMP, JPEG, PNG, ReportBuilder to PDF,
XtremeDocumentStudio includes VCL WMF, EMF and TIFF) PDF/A, XLSX, RTF, HTML,
components for viewing, printing, and XHTML, TXT, SVG, PNG,
converting PDF, DOCX, DOC, RTF, BMP, Display & print DOCX - Support JPEG, GIF and othe formats.
JPEG, PNG, WMF, EMF, and single-page for pages, sections, headers,
and multi-page TIFF. It also has report- footers, columns, paragraphs, Report-export PDF/A support
export components for ACE Reporter, images, text wrapping around includes PDF/A 1b, 2b, and 3b
FastReport, QuickReport and images… versions. PDF/A-3b support
ReportBuilder components that can export complies with ZUGFeRD
to PDF, PDF/A, XLSX, RTF, HTML, XHTML, Display & print PDF - Text, electronic invoicing standard.
TXT, SVG, PNG, JPEG and GIF formats. images, shapes
PDF/A support includes compliance with 100% Delphi Pascal code
PDF/A 1b, 2b, and 3b versions. PDF/A-3b Multi-format FireMonkey
support complies with ZUGFeRD electronic document viewer Support for files, streams and
invoicing standard. In future, creation and DB blobs
editing support will be added. Advanced printing of DOCX,
PDF, BMP, JPEG, PNG, WMF, Unicode text-rendering support
XtremeDocumentStudio also includes EMF and TIFF files
FireMonkey support. Its FireMonkey 12 months free upgrades
document viewer can display PDF, DOC, Conversion support for PDF-to- (major and minor) and priority
RTF and images on Windows, Mac, iOS and RTF, PDF-to-text, PDF-to- e-mail support
Android platforms. The FireMonkey image, DOCX-to-PDF, DOCX-to-
support is currently in beta and we are image, image-to-image exports
continually improving the rendering with
support for more and more features of
each of the formats.
COUPON CODE. “XDOCBP” FOR 25% DISCOUNT ON
XtremeDocumentStudio Delphi is written
XTREMEDOCUMENTSTUDIO DELPHI ULTIMATE.
in 100% Object Pascal for both VCL and
FireMonkey. It can process all supported VALID UNTIL FEB 25, 2017.
formats without requiring external
software such as Microsoft Word, Open
XML SDK, Adobe PDF library or
GhostScript.
All Gnostice Delphi/C++Builder developer Users can go to the Buy Now page at:
tools including XtremeDocumentStudio https://www.gnostice.com/xdoc-delphi
Delphi are available in the highly- and use this to purchase
discounted XtremeDevSystem Delphi XtremeDocumentStudio Delphi Ultimate.
Subscription bouquet.
AN ALTERNATIVE WAY FOR LOCALIZATIONS PAGE 1/9
BY PAUL NAUTA
starter expert
This way of working is quite ok for library functions
and has following advantages:
ABSTRACT
● The language can be changed on the fly,
Localization is the process to adapt your application
you would just need to give ProgramLanguage
to different languages. The standard way of doing
a different value
this in Delphi is via language specific resource files.
● The text strings are defined at the location
This article describes an alternative way for
where they are being used
localization, by using a TClientDataSet to store the
language specific resource strings. Pros and cons of ● A new application using existing library
this approach will be shown. functions would not need to arrange anything
additional for localization of those
library functions
INTRODUCTION But it also has following disadvantages:
The standard way of localization via resource files and ● Addition of an extra language requires an
resource DLL's has several drawbacks: additional set of strings, each stored in the
● Many new files must be created application
● Resource DLL's require one additional project ● With more languages, the IF…THEN statements
per language become rather extensive
● Synchronization across all supported
languages can be quite some work, in many This led me to the approach to store this
different files information in a TClientDataSet.
● Maintenance in case of changes to the
application is in principle straight forward USING TCLIENTDATASET
but in practice very cumbersome and often To store language strings in a DataSet we need a field
only partly executed for every language and an Identifier for the string we
Being a Dutchman, I have projects in Dutch and want to translate.
English, but only a very few projects are both in Dutch
and English. They all share quite some generic libraries This Identifier has some special requirements:
and component libraries so localization of them is ● It must be understandable from the context
important to reduce design work load. To avoid setting of the code where it is being used. Therefore,
up language DLL's for every project, also for the it should be meaningful and thus not a
projects that will never be used in a different language, meaningless number.
I designed a simple set of language options, a global ● It must be unique, to be able to find the correct
variable containing the selected language and several language string. And because we are a little bit lazy,
text strings for each of the languages. Let us look at a it should be case insensitive
simple function to check if a file exists and to report
that to the user: We foresee that different units might access the same
Identifier with a slightly different meaning so it is
useful to extend the unique key with an
TYPE identifier for the unit in which the string
TPKN_Language = ( lanENG, lanNLD ); is used. This will also help identifying
VAR ProgramLanguage : TPKN_Language;
which strings must be changed in case the
FUNCTION CheckFileExists( FileName : String ) : Boolean; application must be changed.
VAR sMessage : String; To access the DataSet we need of course
BEGIN some special functions to set the language
Result := FileExists( FileName ); specific text strings and to read them,
IF Result THEN plus a facility to specify the language.
BEGIN As we might need more dedicated
sMessage := 'File already exists: ' + FileName; functionality in future we will define
IF ( ProgramLanguage = lanNLD ) THEN a derived component to arrange all this:
sMessage := 'Bestand bestaat al: ' + FileName;
END
ELSE
BEGIN
sMessage := 'File does not exist: ' + FileName;
IF ( ProgramLanguage = lanNLD ) THEN
sMessage := 'Bestand bestaat niet: ' + FileName;
END;
ShowMessage( sMessage );
END;
You might notice that I prefix all my own types with my initials, thus TPKN_*. Let us start with
explaining the Constructor, which is implemented as:
CONSTRUCTOR TPKN_Localization.Create( AOwner : TComponent );
VAR sDirectory : String;
BEGIN
INHERITED;
sDirectory := ExtractFilePath( Application.ExeName );
FileName := sDirectory + 'Localizations.cds';
IF NOT FileExists( FileName ) THEN DefineDataSet;
Open;
IndexDefs.Add( 'Identifier', 'LOC_UNITID;LOC_NAME', [ ixUnique, ixCaseInsensitive ] );
IndexName := 'Identifier';
FLanguage := lanENG;
FLanguageTxt := GetEnumName( TypeInfo( TPKN_Language ), Ord( FLanguage ) );
END;
What is this Constructor doing? It defines the DataSet to use as 'Localizations.cds', located in the same
directory as the application. If this file is not yet present, the application will automatically create it via the
DefineDataSet procedure. Because the saved TClientDataSet does not save the index, we will create the index in
the Constructor; it will be needed to find the required language strings. Finally we define the initial language to use as
lanENG. For easy reference, we store the string representation of the language in FLanguageTxt.
The DefineDataSet determines the full field definition of the DataSet and is implemented as follows:
PROCEDURE TPKN_Localization.DefineDataSet;
VAR iLanguage : TPKN_Language; sLanguage : String;
BEGIN
FieldDefs.Clear;
FieldDefs.Add( 'LOC_ID', ftAutoInc );
FieldDefs.Add( 'LOC_UNIT_ID', ftString, 40 );
FieldDefs.Add( 'LOC_NAME', ftString, 30 );
FOR iLanguage := Low( TPKN_Language ) TO High( TPKN_Language ) DO
BEGIN
sLanguage := GetEnumName( TypeInfo( TPKN_Language ), Ord( iLanguage ) );
FieldDefs.Add( 'LOC_TEXT_' + UpperCase( sLanguage ), ftString, 100 );
END;
CreateFields;
CreateDataset;
SaveToFile;
END;
Thus, we create 'LOC_ID' as an ftAutoInc field as the unique record identifier and 'LOC_NAME' as string for the Text
Identifier. The UnitID will be stored in the string field 'LOC_UNIT_ID'. After this we create a field for every language
that is defined in TPKN_Language, using its string representation. So, texts for the language lanNLD will be stored in
the field 'LOC_TEXT_LANNLD', etc. The 'CreateFields' procedure actually creates the fields in the
TClientDataSet, while CreateDataSet will create the DataSet in memory. Via SaveToFile the DataSet
will be saved to the file 'Localizations.cds'. Thus, the developer has to do nothing whenever he implements an instance
of TPKN_Localization, this file will be created automatically if not yet existing.
The SetLanguage procedure is rather straight forward:
The most interesting part is the Exception. Why would it be needed? Well, we could have a situation that after
creation of 'Localizations.cds' we add a new language to TPKN_Language for which no corresponding field
is present yet, so we need to signal it. It is not straight forward to change the field definition of an already existing
TClientDataSet (there is no functionality like RestructureTable) so we leave that out of scope for this demonstration.
Another remark can be made on the error message itself: here we need hard coding of the text string because when
there are errors in this component we will probably not be able to show a language specific message.
This is thus a draw-back, although only a minor one.
The UnitKey parameter is simply the unique identifier for the Unit that calls this procedure, while the reference to
the Text String is given via the Name parameter. The actual string to store is given via the parameter
LanguageText. Of course, we need to specify here also the language for which LanguageText is valid, via the
ALanguage parameter. Finally, the OverWrite parameter is used to define if the value will always be set
(Overwrite = True) or only in case not yet set (thus empty). In the procedure above we use iLanguage to store
the current value of the language setting for later reset and SetLanguage to set the desired language, including the
exception handling. The procedure checks if the required record is already present. If not, the record is created and
the string value is set, otherwise the value is set but only in case OverWrite is True.
To retrieve a language specific text value, we use the GetLanguageText procedure:
This implementation is also rather straight forward, but with some special remarks: in case the Identifier is not
found, it will return an empty value (of course you could define a different solution, for example an exception or an
'Undetermined' value). In case the record is found but the language text is empty, it will return the first language in
the TPKN_Language type, which will normally be the base language in which applications are developed.
Also in this case, we could issue warnings or exceptions, but in many situations it is better to show a base language
than annoying warnings to users.
PRACTICAL EXAMPLE
In view of the situation that we need to retrieve language texts frequently, it is the best approach to create an in
instance of TPKN_Localization only once, during initialization. It is maybe not desired to expose this
DataSet directly to the application; it could lead to unwanted usage, so we resolve this via wrappers for the real
functionality:
IMPLEMENTATION
tabLocalizations : TPKN_Localization;
FUNCTION GetLanguage : String;
BEGIN
Result := tabLocalizations.Language;
END;
In this way, the DataSet will become a local object: tabLocalizations, only accessible via the wrapper functions.
The GetLanguageText function has a number of extra parameters to allow replacing %1, %2 or %3 parameters
by values from the actual context. This is needed because different languages could place such context on different
positions in the text. The SetLanguageText function always sets a value, the SetLanguageTextDefault only
when not yet present.
Now we can rewrite our original code for CheckFileExists to:
FUNCTION CheckFileExists( FileName : String ) : Boolean;
VAR sMessage : String;
BEGIN
Result := FileExists( FileName );
IF Result THEN
sMessage := GetLanguageText( UnitID, 'FileAlreadyExists', FileName )
ELSE
sMessage := GetLanguageText( UnitID, 'FileDoesNotExist', FileName );
ShowMessage( sMessage );
END;
In this case we use only one parameter: FileName, to specialize the text to show to the user.
But we are not yet ready: we need to have a value for UnitID:
CONST
UnitID = '{023F4EF2-0690-4723-865B-3537DCE19B32}'
Here we use a GUID which is truly unique, even across all computers in the world! You can create such GUID
in the IDE via CTRL-SHIFT-G. We could also use the Name of the Unit for this constant but in my experience,
it sometimes happens that the same unit name is used in a different project with a different meaning (most
infamous are the Unit1.pas filenames that are generated automatically by Delphi for new Units).
However, the function would not yet show a decent message, because we still need to define the proper
language texts:
INITIALIZATION
SetLanguageTextDefault( UnitID, 'FileAlreadyExists', 'File "%1" already exists', lanENG );
SetLanguageTextDefault( UnitID, 'FileAlreadyExists', 'Bestand "%1" bestaat al', lanNLD );
SetLanguageTextDefault( UnitID, 'FileDoesNotExist', 'File "%1" does not exist', lanENG );
SetLanguageTextDefault( UnitID, 'FileDoesNotExist', 'Bestand "%1" bestaat niet', lanNLD );
Here we define the texts both for English and Dutch. It is done in the initialization section, thus at start-up of
the application, to have the performance penalty only once.
The coupling between 'FileAlreadyExists' in SetLanguageTextDefault and GetLanguageText is
rather weak, we could make it stronger be defining a constant like:
CONST
lcFileAlreadyExists = 'FileAlreadyExists';
which could change the code to:
sMessage := GetLanguageText( UnitID, lcFileAlreadyExists, FileName )
and
SetLanguageTextDefault( UnitID, lcFileAlreadyExists, 'File "%1" already exists', lanENG );
Any typing error would now lead to a compiler error. In fact, this is quite similar to using ResourceStrings
although the implementation is completely different.
First we check if the Form has any items in the Localizations DataSet. If not, we can stop right-away.
Next we will analyze all components on the form to see if text strings must be adapted:
PROCEDURE TPKN_Localization.SetComponentLanguage( Component : TComponent; UnitKey : String );
VAR iComponent : Integer; sText : String; oMenu : Tmenu; iMenu : Integer;
BEGIN
IF HasProperty( Component, 'Caption' ) THEN
BEGIN
sText := GetLanguageText( UnitKey, Component.Name + '.Caption' );
IF ( sText <> '' ) THEN
SetPropValue( Component, 'Caption', sText );
END;
FOR iComponent := 0 TO Component.ComponentCount - 1 DO
IF ( Component.Components[ iComponent ] IS TMenu ) THEN
BEGIN
oMenu := TMenu( Component.Components[ iComponent ] );
FOR iMenu := 0 TO oMenu.Items.Count - 1 DO
SetMenuLanguage( oMenu.Items[ iMenu ], UnitKey );
END
ELSE
SetComponentLanguage( Component.Components[ iComponent ], UnitKey );
END;
Remember that a form is a component, which owns other components. And these components could
own components as well (like TPanel or TGroupBox). So we need recursive calling of this procedure,
that is the reason why TComponent is given as Parameter and not TForm. Because not every
component has a Caption (for example, TEdit has no Caption), we need to find out if the
component has a Caption. This is done by adding the HasProperty function which looks like:
FUNCTION HasProperty( Comp : TComponent; CONST Prop : String ) : Boolean;
BEGIN
Result := ( GetPropInfo( Comp.ClassInfo, Prop ) <> NIL );
END;
You will need to add System.TypInfo to the USES section to make it functional.
Menus are also components, but their menu items cannot be obtained via the Components property
but only via the Items property. In that case, the procedure SetMenuLanguage must be used:
PROCEDURE TPKN_Localization.SetMenuLanguage( MenuItem : TMenuItem; UnitKey : String );
VAR
sText : String;
iMenu : Integer;
BEGIN
sText := GetLanguageText( UnitKey, MenuItem.Name + '.Caption' );
IF ( sText <> '' ) THEN
MenuItem.Caption := sText;
We do not want to expose all these new procedures to the application; in fact,
we only need a wrapper for SetFormLanguage:
PROCEDURE SetFormLanguage( Form : TForm; UnitKey : String );
BEGIN
tabLocalizations.SetFormLanguage( Form, UnitKey );
END;
DEMO APPLICATION
To see this at work, we create a demo application with a TGroupBox, 2 TButtons, a TComboBox and a TMemo.
We also need a Main Menu and a PopupMenu for the TMemo:
The mnuMain gets 2 top items (mnuTest1 and mnuTest2). The mnuTest2 gets a subitem mnuTest21 and
a subsubitem mnuTest211. The mnuPopup gets 2 items, mnuPopup1 and mnuPopup2. We will set the
default language specific text values as follows:
INITIALIZATION
SetLanguageTextDefault( UnitID, 'frmMain.Caption', 'Localization Demo', lanENG );
SetLanguageTextDefault( UnitID, 'frmMain.Caption', 'Localisatie Demonstratie', lanNLD );
SetLanguageTextDefault( UnitID, 'btnButton1.Caption', 'Check File Exists', lanENG );
SetLanguageTextDefault( UnitID, 'btnButton1.Caption', 'Controleer of Bestand bestaat', lanNLD );
SetLanguageTextDefault( UnitID, 'btnButton2.Caption', 'Change Language', lanENG );
SetLanguageTextDefault( UnitID, 'btnButton2.Caption', 'Verander van Taal', lanNLD );
SetLanguageTextDefault( UnitID, 'mnuTest1.Caption', 'Execute', lanENG );
SetLanguageTextDefault( UnitID, 'mnuTest1.Caption', 'Uitvoeren', lanNLD );
SetLanguageTextDefault( UnitID, 'mnuTest2.Caption', 'Modify', lanENG );
SetLanguageTextDefault( UnitID, 'mnuTest2.Caption', 'Wijzigen', lanNLD );
SetLanguageTextDefault( UnitID, 'mnuTest21.Caption', 'Hide', lanENG );
SetLanguageTextDefault( UnitID, 'mnuTest21.Caption', 'Verbergen', lanNLD );
SetLanguageTextDefault( UnitID, 'mnuTest211.Caption', 'Remove', lanENG );
SetLanguageTextDefault( UnitID, 'mnuTest211.Caption', 'Verwijderen', lanNLD );
SetLanguageTextDefault( UnitID, 'mnuPopup1.Caption', 'Find', lanENG );
SetLanguageTextDefault( UnitID, 'mnuPopup1.Caption', 'Zoeken', lanNLD );
SetLanguageTextDefault( UnitID, 'mnuPopup2.Caption', 'Replace', lanENG );
SetLanguageTextDefault( UnitID, 'mnuPopup2.Caption', 'Vervangen', lanNLD );
SetLanguageTextDefault( UnitID, 'cbxCombo.Items.Text', 'One;Two;Three', lanENG );
SetLanguageTextDefault( UnitID, 'cbxCombo.Items.Text', 'Een;Twee;Drie', lanNLD );
SetLanguageTextDefault( UnitID, 'memMemo.Lines.Text', 'This is a Memo;In English', lanENG );
SetLanguageTextDefault( UnitID, 'memMemo.Lines.Text', 'Dit is een Memo;In het Nederlands', lanNLD );
SetLanguageTextDefault( UnitID, 'gbxGroupBox.Caption', 'File Group Box', lanENG );
SetLanguageTextDefault( UnitID, 'gbxGroupBox.Caption', 'Bestands Groep', lanNLD );
To replace the text as defined in the dfm file by these settings, we need to call SetFormLanguage on
Activation of the form. You might notice the settings for memMemo.Lines.Text and
cbxCombo.Items.Text. The Text property does not exist on these components, in fact these components
have Lines and Items respectively as property, which are TStrings. In the current implementation of
SetComponentLanguage this cannot be translated so we need a workaround in the Activation:
PROCEDURE TfrmMain.FormActivate( Sender : TObject );
BEGIN
SetFormLanguage( Self, UnitID );
cbxCombo.Items.Text := StringReplace( GetLanguageText( UnitID, 'cbxCombo.Items.Text' ),
';', #13#10, [ rfReplaceAll ] );
cbxCombo.Text := cbxCombo.Items[ 0 ];
memMemo.Lines.Text := StringReplace( GetLanguageText( UnitID, 'memMemo.Lines.Text' ),
';', #13#10, [ rfReplaceAll ] );
END;
Please note that we need to replace the semi-colon by CarriageReturn/LineFeed to populate the
properties properly. Also, we set the initial value of the ComboBox to the first value in the Items list.
With the first button, we check if a file exists. A simple implementation could look like:
PROCEDURE TfrmMain.btnButton1Click( Sender : TObject );
BEGIN
CheckFileExists( 'C:\Windows\System32\Calc.exe' );
END;
FormActivate( Sender );
END;
First we set the new required language, then we simply call again FormActivate to
actually change the user interface. Starting the application will show:
CONCLUSION
I have shown that it is very well possible to execute localizations by using a TClientDataSet.
Especially for libraries and generic components this is quite simple. Whenever they are used, localization is
already automatically present without any additional action for the developer, but the localization could be
adapted using an Editor for Localizations.cds. Extra languages could easily be added to
TPKN_Language after which the necessary strings can be added via an Editor. I would recommend to use
SetLanguageTextDefault only for one or two base languages and add other languages via the Editor.
It could even be considered to allow end users to modify text themselves via such an Editor.
The Editor also gives a nice overview of all text strings, for all languages, in one place.
Very convenient for maintenance and standardization!
However, this approach has some drawbacks:
· Complex forms cannot easily be handled in the described way ( it would require rather complex
extensions to SetComponentLanguage with inherent performance penalty)
· Startup of the application will take longer
· Showing a screen will take longer
· The size of the application becomes somewhat larger
· Using huge amounts of text translations and many languages could have impact on the memory
consumption of TClientDataSet
I have the opinion that only the situation of complex forms is a serious one. The others will hardly be
noticeable. Fortunately, there is no need to choose either between the standard Delphi way or using this
approach: they can exist together and even supplement each other!
FMX application on Windows. Showing a binary DOC template file with tables.
25
DOCUMENT TECHNOLOGY FOR VCL AND FMX PAGE 3/3 Gnostice TM
This was an introduction to the new Document Viewer included in Gnostice XtremeDocumentStudio Delphi.
In the next issue we'll go deeper into specific areas of the viewer and explore more application scenarios.
XtremeDocumentStudio is a native Delphi library and does not use external libraries.
The paid Ultimate edition includes full source code.
26 Gnostice
Smart needs...Smarter solutions...
TM
ALines.LoadFromFile( AFileName );
ABitmap.PixelFormat := pf32bit;
APixels := ABitmap.ScanLine[ 0 ];
And drop it on the form: Inc( APixels, IMAGE_WIDTH );
for I := 0 to ALines.Count - 1 do
begin
if((( I mod IMAGE_HEIGHT ) = 0 ) and ( I > 0 )) then
begin
AImageList.AddMasked( ABitmap, clRed );
APixels := ABitmap.ScanLine[ 0 ];
Inc( APixels, IMAGE_WIDTH );
end;
for X := 28 downto 1 do
begin
Dec( APixels );
case( ALines[ I ][ X ] ) of
'+' :
APixels^ := clGray;
Now we can start adding the code.
First, we will declare 2 constants with the '#' :
Width and Height of the images: APixels^ := clBlack;
const else
IMAGE_WIDTH = 28; APixels^ := clWhite;
IMAGE_HEIGHT = 28;
…. end;
end;
end;
Next, in the TForm1 we will add 2 private
sections, one with the fields that we will use, and AImageList.AddMasked( ABitmap, clRed );
ABitmap.Free();
one with the methods: end;
TForm1 = class(TForm)
….
private
FTrainingLabels : ISLRealBuffer;
FTestLabels : ISLRealBuffer;
private
procedure PopulateImageList( AImageList : TImageList; AFileName : String );
function LoadLabels( AFileName : String ) : ISLRealBuffer;
procedure PopulateListViewLabels( AListView : TListView; ALabels : ISLRealBuffer );
procedure PopulateListView( AListView : TListView );
….
begin
AListView.Items.BeginUpdate();
for I := 0 to AListView.Items.Count - 1 do
AListView.Items[ I ].Caption := Round( ALabels[ I ] ).ToString();
AListView.Items.EndUpdate();
end;
The code is very simple. We just get each label And we do the same for the testimages and
from the buffer, round it and set it as caption of testlabels files, populating the TestImageList,
the corresponding TListView item. and TestListView.
Now we need to add the methods to extract the
Now that we have the methods ready, we can use features from the images. We will declare the 2
them to read the files, and populate the methods in the class:
ImageLists and the ListViews when the form is
TForm1 = class(TForm)
created. ….
Switch back to the Form Editor by clicking on the private
“Design” tab. ….
function ExtractImageFeatures( ABmp : TBitmap ) :
Double click on the form to generate the OnCreate ISLRealBuffer;
event: function ExtractAllFeatures( AImageList :
TImageList ) : ISLRealBufferArray;
….
PopulateListView( TrainingListView );
PopulateListViewLabels( TrainingListView, FTrainingLabels );
PopulateListView( TestListView );
PopulateListViewLabels( TestListView, FTestLabels );
end;
Get a pointer to the buffer data: Next, we need to write the event handler when the
APtr := Result.Write();
button is pressed and use the ExtractAllFeatures
Set the Bitmap's PixelFormat to pf32bit, and obtain and the ExtractImageFeatures methods to train
pointer to the pixel data (Please note that TBitmap the classifier, then perform recognitions.
stores the data mirrored, so the first line is actually the To do the recognitions, we will need to add one
last line in the image): more field FResult in the TForm1, where we will
store the result from the classifier event:
ABmp.PixelFormat := pf32bit;
APixels := ABmp.ScanLine[ ABmp.Height - 1 ]; TForm1 = class(TForm)
….
private
Then we iterate through the pixels: ….
FResult : Integer;
for I := 0 to ABmp.Width * ABmp.Height - 1 do
ABitmap.Free();
Result := AResult;
end;
for I := 0 to ATrainingData.Count - 1 do
ILRadialBasisFunctionNetwork1.Train( ATrainingData[ I ], FTrainingLabels[ I ] );
TestListView.Items.BeginUpdate();
else
TestListView.Items[ I ].Caption := FResult.ToString()
+ '(' + Round( FTestLabels[ I ] ).ToString() + ')';
end;
TestListView.Items.EndUpdate();
Label1.Caption := AErrorsCount.ToString()
+ ' Errors of ' + TestImageList.Count.ToString()
+ ' - ' + ( ( TestImageList.Count - AErrorsCount )
* 100 / TestImageList.Count ).ToString() + '% correct';
end;
In the code first we will extract all the features and assign them to ISLRealBufferArray:
ATrainingData := ExtractAllFeatures( TrainingImageList );
Iterate trough all the buffers in the array, and train the classifier:
for I := 0 to ATrainingData.Count - 1 do
ILRadialBasisFunctionNetwork1.Train( ATrainingData[ I ], FTrainingLabels[ I ] );
Once the training is done, we can perform recognitions.
Again, extract features from all the test images:
ATestFeatures := ExtractAllFeatures( TestImageList );
Zero the error counter: AErrorsCount := 0;
Iterate through all the elements and perform the recognition:
for I := 0 to TestImageList.Count - 1 do
begin
ILRadialBasisFunctionNetwork1.Predict( ATestFeatures[ I ] );
Then check the result and update the TListItem captions with it:
if( FResult <> FTestLabels[ I ] ) then
begin
TestListView.Items[ I ].Caption :=
'Error ' + FResult.ToString() + '(' + Round( FTestLabels[ I ] ).ToString() + ')';
Inc( AErrorsCount );
end
else
TestListView.Items[ I ].Caption := FResult.ToString() + '(' + Round( FTestLabels[ I ] ).ToString() +
')';
The only thing that remains to be done is getting The code is very simple. If nothing has been
the result from the recognition from the classifier. recognized, then set the FResult to -1:
Switch to the Form Editor by clicking on the “Design” if( AResult.Count = 0 ) then
tab. begin
Select the ILRadialBasisFunctionNetwork1 FResult := -1;
Exit;
component. In the object inspector click on the
end;
“Events” tab. Double click on the value of the
OnResult event to generate event handler:
Otherwise round and set the recognized category
(Label) into FResult:
In the event handler, FResult := Round( AResult[ 0 ].Neuron.Category );
add the following code:
The application is ready. If we run it, and click on
the "Run" button, we will see the result:
procedure
TForm1.ILRadialBasisFunctionNetwork1Result(ASe
nder: TObject;
AFeatures: ISLRealBuffer; AResult:
TILRadialBasisFunctionResult);
begin
if( AResult.Count = 0 ) then
begin As you can see we have 109 errors over 1000
FResult := -1; images or 89.1 % correct recognition, which is
Exit; not bad for such hand scribbled digits.
end;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.ImageList, Vcl.ImgList,
Vcl.ComCtrls, Mitov.Types, LPComponent, ILBasicClassifier,
ILRadialBasisFunctionNetwork, Vcl.StdCtrls;
type
TForm1 = class(TForm)
TrainingListView: TListView;
TrainingImageList: TImageList;
TestListView: TListView;
TestImageList: TImageList;
Button1: TButton;
Label1: TLabel;
Label2: TLabel;
Label4: TLabel;
ILRadialBasisFunctionNetwork1: TILRadialBasisFunctionNetwork;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure ILRadialBasisFunctionNetwork1Result(ASender: TObject;
AFeatures: ISLRealBuffer; AResult: TILRadialBasisFunctionResult);
private
FTrainingLabels : ISLRealBuffer;
FTestLabels : ISLRealBuffer;
FResult : Integer;
private
procedure PopulateImageList( AImageList : TImageList; AFileName : String );
function LoadLabels( AFileName : String ) : ISLRealBuffer;
procedure PopulateListView( AListView : TListView );
procedure PopulateListViewLabels( AListView : TListView; ALabels : ISLRealBuffer );
function ExtractImageFeatures( ABmp : TBitmap ) : ISLRealBuffer;
function ExtractAllFeatures( AImageList : TImageList ) : ISLRealBufferArray;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
Mitov.Containers.List;
const
IMAGE_WIDTH = 28;
IMAGE_HEIGHT = 28;
for I := 0 to ALines.Count - 1 do
Result[ I ] := ALines[ I ].ToInteger();
end;
AListView.Items.EndUpdate();
end;
for I := 0 to ATrainingData.Count - 1 do
ILRadialBasisFunctionNetwork1.Train( ATrainingData[ I ], FTrainingLabels[ I ] );
TestListView.Items.BeginUpdate();
TestListView.Items.EndUpdate();
Label1.Caption := AErrorsCount.ToString()
+ ' Errors of ' + TestImageList.Count.ToString() + ' - '
+ ( ( TestImageList.Count - AErrorsCount ) * 100 / TestImageList.Count ).ToString() + '% correct';
end;
end.
TForm1 = class(TForm)
private
…
FCountZones : Integer;
…
Inc( APixels );
Inc( APtr );
end;
VLConnectedComponents1.ProcessFrame(
The application is ready. If we run it, and
TVLImageBuffer.CreateBmp( ABmp ) );
click on the “Run” button, we will see the
Result[ IMAGE_WIDTH * IMAGE_HEIGHT ] := FCountZones / 5; result:
end;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.ImageList, Vcl.ImgList,
Vcl.ComCtrls, Mitov.Types, LPComponent, ILBasicClassifier,
ILRadialBasisFunctionNetwork, Vcl.StdCtrls, SLCommonFilter, VLCommonFilter,
VLConnectedComponents;
type
TForm1 = class(TForm)
TrainingListView: TListView;
TrainingImageList: TImageList;
TestListView: TListView;
TestImageList: TImageList;
Button1: TButton;
Label1: TLabel;
Label2: TLabel;
Label4: TLabel;
ILRadialBasisFunctionNetwork1: TILRadialBasisFunctionNetwork;
VLConnectedComponents1: TVLConnectedComponents;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure ILRadialBasisFunctionNetwork1Result(ASender: TObject;
AFeatures: ISLRealBuffer; AResult: TILRadialBasisFunctionResult);
procedure VLConnectedComponents1Components(Sender: TObject;
Count: Cardinal);
private
FTrainingLabels : ISLRealBuffer;
FTestLabels : ISLRealBuffer;
FResult : Integer;
FCountZones : Integer;
private
procedure PopulateImageList( AImageList : TImageList; AFileName : String );
function LoadLabels( AFileName : String ) : ISLRealBuffer;
procedure PopulateListView( AListView : TListView );
procedure PopulateListViewLabels( AListView : TListView; ALabels : ISLRealBuffer );
function ExtractImageFeatures( ABmp : TBitmap ) : ISLRealBuffer;
function ExtractAllFeatures( AImageList : TImageList ) : ISLRealBufferArray;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
Mitov.Containers.List, System.Math, VCL.Mitov.ImageBuffer;
const
IMAGE_WIDTH = 28;
IMAGE_HEIGHT = 28;
for I := 0 to ATrainingData.Count - 1 do
ILRadialBasisFunctionNetwork1.Train( ATrainingData[ I ], FTrainingLabels[ I ] );
TestListView.Items.BeginUpdate();
else
TestListView.Items[ I ].Caption := FResult.ToString()
+ '(' + Round( FTestLabels[ I ] ).ToString() + ')';
end;
TestListView.Items.EndUpdate();
end.
In this article, I showed you that selecting what features are sent to the classifier affects the
quality of the recognition. Sending more relevant features results in better recognition.
The classifier training and recognition is a slow and CPU consuming process.
In the following articles, we will look into ways to improve the training and recognition
performance by utilizing parallel execution over multiple cores, or if a programmable OpenCL
enable GP GPU is available on the system, by executing the code in the GPU cores.
- Multimonitor remote desktop V5 (VCL and FMX) kbmMemTable is the fastest and most feature rich in
- Rad studio and Delphi 2009 to memory table for Embarcadero products.
Latest 10.1 BERLIN support - Easily supports large datasets with millions of records
- Win32, Win64, Android, IOS 32, IOS 64 - Easy data streaming support
and OSX client and server support! - Optional to use native SQL engine
- Native PHP, Java, OCX, ANSI C, C#, - Supports nested transactions and undo
Apache Flex client support! - Native and fast build in M/D, aggregation /grouping,
- High performance LZ4 and Jpeg compression range selection features
- Native high performance 100% developer defined app - Advanced indexing features for extreme performance
server with support for loadbalancing and failover
COMPONENTS
DEVELOPERS 4 DX
EESB, SOA,MoM, EAI TOOLS FOR INTELLIGENT SOLUTIONS. kbmMW IS THE PREMIERE N-TIER PRODUCT FOR DELPHI /
C++BUILDER BDS DEVELOPMENT FRAMEWORK FOR WIN 32 / 64, .NET AND LINUX WITH CLIENTS RESIDING ON WIN32 / 64, .NET, LINUX, UNIX
MAINFRAMES, MINIS, EMBEDDED DEVICES, SMART PHONES AND TABLETS.