Archive
SMS fonts, once and for all, part 2
In the previous article on this topic, we covered the ported “font detection” JS library which is now a part of QTXLibrary (just update your SVN repository and you have it. The class can be found in the file qtxutils.pas).
As mentioned yesterday, detecting if the browser supports a font doesn’t give us any real tangible information about which font has actually been selected by the browser for an element. A calculated style is, after all, calculated and managed by the browser – not the stylesheet. A stylesheet does nothing more than tell the browser what it would like to have, but it’s the browser that calls all the shots and will override your styles (naturally, if a font is not on your system the browser cant magically invent it) if it has to.
But as luck would have it, being able to detect if a font is installed just what we need to figure out if it’s applied to an element!
In short, here is what we do to figure out what font the browser has selected for an element:
- Read the font-family string for the element
- Split the string into an array, separated by “,”
- Iterate through the array, detecting if each item is installed
- Select the first valid font, since that’s the rule defined by WC3
We should also try to expand this in the future by adding support for:
- Px vs. pt calculations (using body font as % basis for points)
- Percent support (same as above, but percentage of defined document size)
- Inherited keyword support
- Initial keyword support
While I dont intend to deal with the latter list right now, we at least have a game plan! But.. we are also going to take this one step further and introduce something very valuable, namely content pre-calculation.
Content pre-calculation and measurement
Imagine you have to code a news-app for Android and iOS. Just like Facebook and twitter you have to display a piece of a message (or indeed, the whole message if it’s reasonable in size) in a scrollable list. Just like I do in the CaseBook demo app. It’s also a common task when working on e-books or anything which requires dynamic display of content.
The question of “how many pixels will my text occupy” will arise in your mind rather quickly. And like all problems dealing with content rendering (be it under Delphi or Smart Mobile), what you already know are the only factors you can use to truly pre-calculate the correct end result.
In our case we have only one fixed constant, namely the width of the display (depends only on how the user holds the mobile device), while the height factor is flexible (since we have a scrolling display). So the height is unknown and determined completely by the length of the text, the size of the font and ultimately how the browser breaks the content down within the boundaries of the width.
So once we are able to determine the font used by a HTML element and it’s size, being able to pre-calculate what a text message (or a compound message which may include images and other HTML content) looks like becomes the logical next step. And a very valuable and important one, since we are dealing with HTML and browser based app’s after all.
Well, I’m not going to hold you in suspense any longer, here is the final result (below). Again, this code is stored on Google Code, so just update your local SVN repository copy and that’s it. If you install for the first time, remember to check-out the SVN trunk under your SMS/Libraries folder – otherwise the Smart Mobile Studio IDE wont be able to find the files.
Well, here it is – the evolved version of Font Detector library (JS):
type TQTXFontInfo = Record fiName: String; fiSize: Integer; function toString:String; End; TQTXFontDetector = Class(TObject) private FBaseFonts: array of string; FtestString: String = "mmmmmmmmmmlli"; FtestSize: String = '72px'; Fh: THandle; Fs: THandle; FdefaultWidth: Variant; FdefaultHeight: Variant; public function Detect(aFont:String):Boolean; function MeasureText(aFontInfo:TQTXFontInfo; aContent:String):TQTXTextMetric;overload; function MeasureText(aFontInfo:TQTXFontInfo; aFixedWidth:Integer; aContent:String):TQTXTextMetric;overload; function MeasureText(aFontName:String;aFontSize:Integer; aContent:String):TQTXTextMetric;overload; function MeasureText(aFontName:String;aFontSize:Integer; aFixedWidth:Integer; aContent:String):TQTXTextMetric;overload; function getFontInfo(const aHandle:THandle):TQTXFontInfo; Constructor Create;virtual; End; //############################################################################ // TQTXFontInfo //############################################################################ function TQTXFontInfo.toString:String; begin result:=Format('%s %dpx',[fiName,fiSize]); end; //############################################################################ // TQTXFontDetector //############################################################################ Constructor TQTXFontDetector.Create; var x: Integer; begin inherited Create; FBaseFonts.add('monospace'); FBaseFonts.add('sans-serif'); FBaseFonts.add('serif'); Fh:=browserApi.document.body; Fs:=browserApi.document.createElement("span"); Fs.style.fontSize:=FtestSize; Fs.innerHTML := FtestString; FDefaultWidth:=TVariant.createObject; FDefaultHeight:=TVariant.createObject; if FBaseFonts.Count>0 then for x:=FBaseFonts.low to FBaseFonts.high do begin Fs.style.fontFamily := FbaseFonts[x]; Fh.appendChild(Fs); FdefaultWidth[FbaseFonts[x]] := Fs.offsetWidth; FdefaultHeight[FbaseFonts[x]] := Fs.offsetHeight; Fh.removeChild(Fs); end; end; function TQTXFontDetector.getFontInfo(const aHandle:THandle):TQTXFontInfo; var mName: String; mSize: Integer; mData: Array of string; x: Integer; Begin result.fiSize:=-1; if aHandle.valid then begin mName:=w3_getStyleAsStr(aHandle,'font-family'); mSize:=w3_getStyleAsInt(aHandle,'font-size'); if length(mName)>0 then begin asm @mData = (@mName).split(","); end; if mData.Length>0 then Begin for x:=mData.low to mData.high do begin if Detect(mData[x]) then begin result.fiName:=mData[x]; result.fiSize:=mSize; break; end; end; end; end; end; end; function TQTXFontDetector.MeasureText(aFontInfo:TQTXFontInfo; aFixedWidth:Integer; aContent:String):TQTXTextMetric; Begin result:=MeasureText(aFontInfo.fiName,aFontInfo.fiSize,aFixedWidth,aContent); end; function TQTXFontDetector.MeasureText(aFontInfo:TQTXFontInfo; aContent:String):TQTXTextMetric; Begin result:=MeasureText(aFontInfo.fiName,aFontInfo.fiSize,aContent); end; function TQTXFontDetector.MeasureText(aFontName:String;aFontSize:Integer; aFixedWidth:Integer; aContent:String):TQTXTextMetric; var mElement: THandle; Begin if Detect(aFontName) then begin aContent:=trim(aContent); if length(aContent)>0 then begin mElement:=BrowserAPi.document.createElement("div"); if (mElement) then begin mElement.style['font-family']:=aFontName; mElement.style['font-size']:=TInteger.toPxStr(aFontSize); mElement.style['overflow']:='scroll'; mElement.style.maxWidth:=TInteger.toPxStr(aFixedWidth); mElement.style.width:=TInteger.toPxStr(aFixedWidth); mElement.style.height:='10000px'; mElement.innerHTML := aContent; Fh.appendChild(mElement); mElement.style.width:="4px"; mElement.style.height:="4px"; result.tmWidth:=mElement.scrollWidth; result.tmHeight:=mElement.scrollHeight; Fh.removeChild(mElement); end; end; end; end; function TQTXFontDetector.MeasureText(aFontName:String;aFontSize:Integer; aContent:String):TQTXTextMetric; var mElement: THandle; Begin if Detect(aFontName) then begin aContent:=trim(aContent); if length(aContent)>0 then begin mElement:=BrowserAPi.document.createElement("div"); if (mElement) then begin mElement.style['font-family']:=aFontName; mElement.style['font-size']:=TInteger.toPxStr(aFontSize); mElement.style['overflow']:='scroll'; mElement.style['display']:='inline-block'; mElement.style['white-space']:='nowrap'; mElement.style.width:='10000px'; mElement.style.height:='10000px'; mElement.innerHTML := aContent; Fh.appendChild(mElement); mElement.style.width:="4px"; mElement.style.height:="4px"; result.tmWidth:=mElement.scrollWidth; result.tmHeight:=mElement.scrollHeight; Fh.removeChild(mElement); end; end; end; end; function TQTXFontDetector.Detect(aFont:String):Boolean; var x: Integer; Begin aFont:=trim(aFont); if aFont.Length>0 then Begin if FBaseFonts.Count>0 then for x:=FBaseFonts.low to FBaseFonts.high do begin Fs.style.fontFamily:=aFont + ',' + FbaseFonts[x]; Fh.appendChild(Fs); result:= (Fs.offsetWidth <> FdefaultWidth[FBaseFonts[x]]) and (Fs.offsetHeight <> FdefaultHeight[FBaseFonts[x]]); Fh.removeChild(Fs); if result then break; end; end; end;
Now things are much easier to work with! In fact, we can now measure the width and height of a message by simply doing this:
Procedure TForm1.Test; var mObj:TQTXFontDetector; mInfo: TQTXFontInfo; Begin mObj:=TQTXFontDetector.Create; mInfo:=mObj.MeasureText(mObj.getFontInfo(self.handle), 'this is cool!<br>And this is even cooler'); showmessage(mInfo.toString); End;
Now we can measure any HTML segment no matter what it is, be it a compound string which contains images and text combined – even including fancy css fonts like Font Awesome. This gives us a huge advantage over those poor guys writing JavaScript by hand.
Isolating use
Like all great snippets, automating and isolating it’s use is a must. At the moment this piece of gold is safely tucked away in the QTXLibrary – but I plan to include this after more rigourous testing and, if time permits for another hotfix, support for inherited and initial keywords – which is much more complex than it sounds.
The initial and inherited CSS keywords means that we have to recursively query the parent and keep on going until we find a valid font declaration. This also means that I have to carefully sculpt the code to minimize expensive round-trips (so you dont want to put inherited font use on an element under a time critical segment of your app) and slow performance.
Hopefully I will be able to marry TQTXFontDetector with TW3Fonts (the latter name will be kept) to provide the best, easiest to use and most powerful HTML5 font manager out there. And considering that not even jQuery have functions like I have provided here now, that should not be to hard 🙂
Well, enjoy!
SMS fonts, once and for all, part 1
While Smart Mobile Studio makes HTML5 app development fun and enjoyable, there are aspects of HTML5 and CSS3 which quite frankly cannot be simplified without a lot of research. This has nothing to with the top-level programming language, be it Smart Pascal or Typescript, but rather how browsers are designed, their abhorrent need for legacy support for all manner of whimsical ideas over the past 15 years – and last but not least, shortcomings of CSS and Javascript combined.
One of these topics is font management, which (at the present moment is driving me insane) you could be mistaken for being an issue of the 90’s and not 2014. But you will quickly realize that despite all the advancements of HTML5 there are still huge holes in the .. eh, whole WC3 scheme of things.
Now there used to be a time when you could, anywhere in a HTML segment, simply get away with:
<font name="verdana" size="14px">Some text</font>
But alas those days are gone (and we should be happy about that), and the free-standing font tag has been marked as obsolete for quite some time.
For Smart Pascal, how you style your components is really not the issue, but rather – getting concurrent and reliable information from the browser is. You should think that something as trivial as asking what font is used for an element would be simple and straight-forward, but sadly that is not the case.
For instance, you should imagine that you could do something like this:
var mSize = element.style.fontSize; //works var mName = element.style.fontName; //nope
The above wont even work, not even the first. In order to read a calculated style you have to access the browser’s internal workings like this:
mObj := BrowserAPI.Document.defaultView.getComputedStyle(tagRef, Null); if (mObj) then Result := (mObj).getPropertyValue(aStyleName);
But all this aside. The point of this article is to make more sense of fonts under HTML5. As the designer of SMS up to version 1.0 I know perfectly well how much work has gone into this, and also how many aspects of SMS that still needs a polish. And font management is absolutely one of them. Not because we did anything wrong, but because coding clever font detection and/or analysis requires more work than anyone could be expected to anticipate. Smart gives you a huge advantage over raw HTML5 authoring in plain JavaScript – but it can be done better and more accurate. Which is what I am going to do now once and for all (insert sound of trumpet’s going off in the distance here).
Before we dive into some code, here are a few things you can do to alleviate the problem straight away.
Clean up the default stylesheet
At the very top of the stylesheet for your app, is a strange segment which looks like this:
* { //content here }
Just like under SQL, the “*” (star) character means “all”. So whatever you define within those curly-brackets’s is automatically applied to ALL elements (read: all controls) in your app.
So this is the perfect place to clearly define the font-name you want to use in your app, and it’s size.
So if you do like this:
* { font-family: "Segoe UI", "Helvetica neue"; font-color: Transparent; }
Then the default font for the document and all the elements created by Smart will be (and this is important) either “Segoe UI” (Windows 7 and 8) or, if that is not available, “Helvetica neue” (which is the font for iPhone and iPad). You may want to add Helvetica to that list, just in case your app is running under android.
The next step is to completely remove all “font-family” declarations besides that one, from the entire CSS file. This will ensure that unless you have hard-coded a font into your app, you will be able to control font handling from your stylesheet completely. Which is very handy!
Getting the name and size of a font
Let me explain the problem clearly. When you define what font should be used, there is no guarantee that this font is indeed installed on the system running you app. This is why the CSS “font-family” accepts more than one font name. And the rule is, that if “Segoe UI” (first in list) is not installed, then the browser tries to use the next one – and so fourth.
The problem? Whenever you try to read-back what font was actually selected, well.. there are no such function (!) The only thing you can do is to read back the whole “font-family” string which contains exactly what you already know. I was hoping the computed style contained just the applied font and not the whole list, but no such luck.
So what is the solution? Well, there are JS libraries for checking if a font is installed on the system – and the only thing you can actually do about this, is to iterate through the font-family string and use the first one which is valid.
One such solution is FontUnStack, which sadly has a dependency on jQuery. Which is not something I want to ship with Smart Mobile Studio to be perfectly frank. A second solution is Font Detector which, since it has no dependencies, is the one I have re-implemented in Smart Pascal and which we will be using.
But we are still not out of the woods, because while detecting if a font is installed is all nice and dandy, we still have to make a little snippet which uses the font detector to compare against the font-family list we know. Then, and only then, will we have fixed the problem of being able to determine what font is used for an element (phew!).
But let us begin with the font detection class, which turned out very small (based on the 0.3 branch):
type TQTXFontDetector = Class(TObject) private FBaseFonts: array of string; FtestString: String = "mmmmmmmmmmlli"; FtestSize: String = '72px'; Fh: THandle; Fs: THandle; FdefaultWidth: Variant; FdefaultHeight: Variant; public function Detect(aFont:String):Boolean; Constructor Create;virtual; End; //############################################################################ // TQTXFontDetector //############################################################################ Constructor TQTXFontDetector.Create; var x: Integer; begin inherited Create; FBaseFonts.add('monospace'); FBaseFonts.add('sans-serif'); FBaseFonts.add('serif'); Fh:=browserApi.Document.getElementsByTagName("body")[0]; Fs:=browserApi.document.createElement("span"); Fs.style.fontSize:=FtestSize; Fs.innerHTML := FtestString; FDefaultWidth:=TVariant.createObject; FDefaultHeight:=TVariant.createObject; if FBaseFonts.Count>0 then for x:=FBaseFonts.low to FBaseFonts.high do begin Fs.style.fontFamily := FbaseFonts[x]; Fh.appendChild(Fs); FdefaultWidth[FbaseFonts[x]] := Fs.offsetWidth; FdefaultHeight[FbaseFonts[x]] := Fs.offsetHeight; Fh.removeChild(Fs); end; end; function TQTXFontDetector.Detect(aFont:String):Boolean; var x: Integer; Begin aFont:=trim(aFont); if aFont.Length>0 then Begin if FBaseFonts.Count>0 then for x:=FBaseFonts.low to FBaseFonts.high do begin Fs.style.fontFamily:=aFont + ',' + FbaseFonts[x]; Fh.appendChild(Fs); result:= (Fs.offsetWidth <> FdefaultWidth[FBaseFonts[x]]) and (Fs.offsetHeight <> FdefaultHeight[FBaseFonts[x]]); Fh.removeChild(Fs); if result then break; end; end; end;
You can try this out yourself, and you will find that it absolutely works and easily detects if a font is installed on your system. So great, that’s the first part of the equation finished.
More to follow soon..
Creating a modal dialog in Smart Mobile Studio
Just a quick tip for all you SMS hackers out there. Have you ever wanted to create a modal dialog for your SMS applications? Well, the bad news is that JavaScript doesnt know a modal form if it’s life depended on it — but the good news is, that you can force the user to take notice, creating the same desired affect.
The way to make the user take notice is, naturally, to make it impossible for him/her to continue doing anything until your dialog is released. Now there are many ways to do this, from the extremely anoying to the almost pointless. In our case we will be simple and to the point (which I think most users favour) and simply create a block-box.
The block-box approach
What is a block-box? Well, it’s actually just something that is semi-transparent and covers the whole display — blocking all user input from ever reaching underlying components. Clever right?
You will find the class TW3BlockBox defined in W3Application.pas and all it does is to fill the display completely with a black, semi-transparent panel. If you have ever user TApplication.ShowDialog then you know what it looks like.
Creating a block box is as simple as:
FBlocker := TW3BlockBox.Create(Application.Display); FBlocker.SetBounds(0,0,Application.Display.Width,Application.Display.Height); FBlocker.BringToFront;
And voila, you have effectively blocked the entire display from getting any user-input events (so make sure your panel or dialog release the FBlocker instance, or you will never get out again.
When you create your dialog, remember to use FBlocker as parent (!)
Also, if you are worrying about “oh what if i forget to release it? What about memory”, well that’s not a problem. With the block-box in place the user cannot do anything until you release it (unless you have a timer that kicks in and does something fantastically illegal) – and secondly, releasing your instance is very simple too.
I suggest you do like me, and isolate your dialog in a class. Create the block-box inside your constructor. Then have a “ok” button (or whatever you want the user to do) and in that button you just go:
Procedure TMyDialog.W3Button2Click(Sender:TObject) Begin if assigned(FOnDialogDone) then FOnDialogDone(self,fmOk); w3_callback( procedure () begin self.free; end, 100); end;
This will call back to the main app with your results, and gracefully release the dialog 100ms later. W3_Callback is important because it gives the browser time to update stuff, and since we now write async code – that is a very important factor. How you sculpt the dialog and/or events is up to you. Also, javascript is managed, so there is no such thing as memory loss (in theory) unless you create 1 million nested divs and just keep on going 🙂
Built in modal functions
The RTL contains functionality for dealing with modal dialogs in a more uniform way. If you head over to Primoz’s website you will find a full tutorial on using that functionality: http://www.smartprogrammer.org/2013/04/new-in-smart-11-modal-dialogs.html
Enjoy!
QTX updated + casebook
As promised in the previous article, I have replaced the standard TW3HeaderControl with the new one we made yesterday. To make it easier to spot the effects on the title, i have turned it’s background red.
Head over to http://lennartaasenden.magix.net/public/ and have a peek at the new header
A better IPhone Header for Smart Mobile Studio
With the introduction of the QTX effects units, we can start to make some interesting components that feel more responsive and alive (for the lack of a better word). If the world of HTML5 apps is about anything – it’s flamboyant and exciting user interfaces which not only rival, but surpass the classical components of native languages.
Since writing your own components is often regarded as a black art, something you really dont want to do which is time consuming and boring – I have decided to show just how little it takes to make a completely new IOS header component for Smart Mobile Studio. The goals for this new header is simple, namely to mimic some of the native effects introduced in iOS 5.x – which in no particular order are:
- When either back or next buttons are set to invisible, they slide out of view
- When a button slides out, the caption follows suit – taking up the newly available space
- When you alter the title of the header, the text fades out before setting the new text, then fades back in
- The title font is not longer weight-bold, but plain and shaded
Sounds very complicated right? Well, under Delphi or C++ builder it would have required more than a fair share of black magic, but under Smart Mobile Studio – which were built for this very purpose, it’s a snap.
Isolating the parts
First, let’s look at what an IOS header consists of. It’s actually a very simple piece of engineering, with only two possible buttons visible at once, a gradient background (and buttons styled to match) and a caption. Depending on your IOS version the title is either centered or left oriented. We are going to opt for a centered caption.
So, what we end up with are just 3 custom controls. That’s all it’s going to take to make our new and exciting IOS header, which will look oh-so-clever in combination with sliding forms whizzing about the place. So let’s get to work.
First, we isolate the button classes:
TQTXBackButton = Class(TW3CustomControl) private FOnVisible: TQTXButtonVisibleEvent; protected procedure setVisible(const aValue:Boolean);reintroduce; public property OnVisibleChange:TQTXButtonVisibleEvent read FOnVisible write FOnVisible; published Property Visible:Boolean read getVisible write setVisible; End; TQTXNextButton = Class(TW3CustomControl) protected procedure setVisible(const aValue:Boolean);reintroduce; End;
Since we want our buttons to be more responsive when they either appear or go-away, we target the visible property on our button components. We also need to inform our toolbar that a button has appeared or disappeared, so we add an event to the mix – simply called OnVisibleChange. This way, we can create a response which deals with re-positioning the title whenever a button is moving.
Next, we need the actual header control. This will be extremely simple since all it’s gonna do is to house 3 child components (two buttons and a label). So that looks like this:
TQTXHeaderBar = Class(TW3CustomControl) private FBackButton: TQTXBackButton; FNextButton: TQTXNextButton; FCaption: TQTXHeaderTitle; Procedure HandleBackButtonVisibleChange(sender:TObject;aVisible:Boolean); protected Procedure Resize;override; Procedure InitializeObject;override; Procedure FinalizeObject;Override; public Property Title:TQTXHeaderTitle read FCaption; Property BackButton:TQTXBackButton read FBackButton; property NextButton:TQTXNextButton read FNextButton; End;
Using the force
With the classes clearly defined, we can start to add some meat to our spanking new component. But first, let’s talk a bit about timing.
When you change the title of what is ultimately a label, you want it execute the change in sequence. First, you want the label to quickly fade out using a smooth effect, then you want the actual text to be altered (invisible at this point), until we quickly fade the label back into view.
This probably sounds very complicated, but it’s really the most simple thing to deal with in this task. In fact, the entire label class is no larger than this:
Procedure TQTXHeaderTitle.SetInheritedCaption(const aValue:String); Begin inherited setCaption(aValue); end; Procedure TQTXHeaderTitle.setCaption(const aValue:String); begin if ObjectReady and TQTXTools.getElementInDOM(Handle) then Begin self.fxZoomOut(0.3, procedure () Begin setInheritedCaption(aValue); self.fxZoomIn(0.3); end); end else inherited setCaption(aValue); end;
You may be wondering why there is a function called “setInheritedCaption” defined. To make a long story short, you can only call inherited methods inside a normal procedure, hence we cant call “inherited setCaption()” to change the caption inside a callback procedure. The only way to solve this is to isolate it outside the setCaption() method.
Now in the setCaption() method we first check if the object is ready to be used, that the handle has become a part of the DOM (document object model). If we omit this, you stand at risk of triggering an effect before the DOM is loaded – and your app wont work. You notice that if things are not ready, we call the ancestor directly, because the text being applied is set from the constructor – so no effect can be applied.
If everything is ready and the component is visible, then we use the QTX effects to zoom the label out in 0.3 seconds. We also provide a callback event (anonymous procedure) which is executed when the effect is done. Here we change the text via setInheritedCaption() and fade the component back in. Easy as apple pie.
Next is the button(s). Like mentioned we wanted an event to let us know whenever a button has it’s visible property changed. The event handler for that presently looks like this:
Procedure TQTXHeaderBar.HandleBackButtonVisibleChange (sender:TObject;aVisible:Boolean); Begin case aVisible of false: Begin FCaption.fxMoveTo(2, (clientHeight div 2) - (FCaption.height div 2), 0.3); end; true: Begin FCaption.fxMoveTo((clientwidth div 2) - (FCaption.width div 2), (clientHeight div 2) - (FCaption.height div 2), 0.3); end; end; end;
So, whenever you alter the visibility of the back-button (button on the left of the header), we either slide the header-label center, or backwards, to occupy the space BackButton used to have.
And lest we forget, here is the code for the actual header. All it does at the moment is to house the other controls, so it’s not that advanced. But it’s a lot more responsive than the default IOS header which ship with Smart Mobile Studio.
Procedure TQTXHeaderBar.InitializeObject; Begin inherited; FBackButton:=TQTXBackButton.Create(self); FBackbutton.InnerHTML:='&lt;b&gt;Back&lt;/b&gt;'; FBackbutton.Background.FromColor(clRed); FBackButton.OnVisibleChange:=HandleBackButtonVisibleChange; FNextButton:=TQTXNextButton.Create(self); FCaption:=TQTXHeaderTitle.Create(self); FCaption.Background.FromColor(clRed); end; Procedure TQTXHeaderBar.FinalizeObject; Begin FBackbutton.free; FNextButton.free; FCaption.free; inherited; end; Procedure TQTXHeaderBar.HandleBackButtonVisibleChange (sender:TObject;aVisible:Boolean); Begin case aVisible of false: Begin FCaption.fxMoveTo(2, (clientHeight div 2) - (FCaption.height div 2), 0.3); end; true: Begin FCaption.fxMoveTo((clientwidth div 2) - (FCaption.width div 2), (clientHeight div 2) - (FCaption.height div 2), 0.3); end; end; end; Procedure TQTXHeaderBar.Resize; Begin inherited; FBackbutton.setbounds(2,2,100,24); FNextButton.setBounds((clientwidth-2)-100,2,100,24); if FBackbutton.Visible then FCaption.MoveTo((clientwidth div 2) - (FCaption.width div 2), (clientHeight div 2) - (FCaption.height div 2)) else Begin FCaption.moveto(2, (clientHeight div 2) - (FCaption.height div 2) ); end; end;
Adding some bling
Before we start working on the NextButton (right on the header), let’s have a peek at what we got so far, and let’s add some dummy events to the mix to make sure the effects are working. First, add the following to InitializeComponent:
FCaption.OnClick:=Procedure (sender:TObject) Begin FBackButton.Visible:=not FBackbutton.Visible; end;
So whenever you click on the title-label, this code will toggle the visibility of our back-button. Next, let’s create an instance of our header and add another event, just for fun. So in the initializeObject on your form, create the IOS header like this:
mTemp:=TQTXHeaderBar.Create(display); mTemp.height:=40; mTemp.BackButton.OnClick:=Procedure (sender:TObject) Begin mTemp.Title.Caption:='Testing changes'; w3_callback( procedure () Begin mTemp.title.caption:='And this is cool stuff'; end, 1000); end;
This means that whenever we click the back-button, we alter the caption twice after each other. Now let’s have a peek at what it looks like.
Meeeh.. let’s try that again, and this time with the CSS used by TW3HeaderControl
Final touches
As you probably anticipate already, the Next-Button is more or less a clone of what we did with the first, so I wont cover that. The difference is that the label wont move so much – as resize itself to the new available size.
What we must do however, is to change the ancestor for the button classes to TW3ToolButton (as opposed to TW3CustomControl) – and basically, that’s it! Less than 100 lines of code and you have a fully effect driven clone of the IOS form header.
Now when we navigate our forms the GUI feels so much more alive and responsive. It’s just an optical effect really, but it gives that special “feel” to it which we recognize from native, Objective C components under IOS.
After thought
This example of writing components was extremely simple, but still the topics involved, like using GPU powered CSS effects is right up there with the best of code. Under native Delphi or C++ we would have to code even that, so naturally the amount of source-code we have to write under those languages would be much, much larger.
But that is also the point, namely to point out how simple, elegant and even enjoying it is to write your own HTML5 components. I know it sounds like i’m just basking in my own ideas, but Smart Mobile Studio is the only product which made me feel like I was 15 years old again, hacking away on my Amiga. Nothing is nicer than waking up, grabbing a fresh pot of coffey and sit down with SMS to code something you love working on.
As always, remember to download the Quartex library (which is needed for the effects), this can be found here: https://code.google.com/p/qtxlibrary/ (and check it out into the SMS/Libraries folder).
Here is the full source so far (the finished version will be on QTX tomorrow):
unit qtxheader; //############################################################################# // // Unit: qtxheader.pas // Author: Jon Lennart Aasenden [Cipher Diaz of Quartex] // Company: Jon Lennart Aasenden LTD // Copyright: Copyright Jon Lennart Aasenden, all rights reserved // // About: This unit introduces a replacement for TW3HeaderControl. // It uses CSS3 animation effects to slide and fade header // elements out of view, which makes for a more responsive // and living UI experience. // // // _______ _______ _______ _________ _______ // ( ___ )|\ /|( ___ )( ____ )\__ __/( ____ \|\ /| // | ( ) || ) ( || ( ) || ( )| ) ( | ( \/( \ / ) // | | | || | | || (___) || (____)| | | | (__ \ (_) / // | | | || | | || ___ || __) | | | __) ) _ ( // | | /\| || | | || ( ) || (\ ( | | | ( / ( ) \ // | (_\ \ || (___) || ) ( || ) \ \__ | | | (____/\( / \ ) // (____\/_)(_______)|/ \||/ \__/ )_( (_______/|/ \| // // // //############################################################################# interface uses W3System, w3components, w3graphics, w3ToolButton, w3borders, qtxutils, qtxeffects, qtxlabel; {.$DEFINE USE_ANIMFRAME_SYNC} const CNT_ANIM_DELAY = 0.22; type TQTXButtonVisibleEvent = Procedure (sender:TObject;aVisible:Boolean); (* Isolate commonalities for Back/Next buttons in ancestor class *) TQTXHeaderButton = Class(TW3ToolButton) private FOnVisible: TQTXButtonVisibleEvent; public property OnVisibleChange:TQTXButtonVisibleEvent read FOnVisible write FOnVisible; Protected Procedure setInheritedVisible(const aValue:Boolean); End; (* Back-button, slides to the left out of view *) TQTXBackButton = Class(TQTXHeaderButton) protected procedure setVisible(const aValue:Boolean);reintroduce; published Property Visible:Boolean read getVisible write setVisible; End; (* Next-button, slides to the right out of view *) TQTXNextButton = Class(TQTXHeaderButton) protected procedure setVisible(const aValue:Boolean);reintroduce; published Property Visible:Boolean read getVisible write setVisible; End; (* Header title label, uses fx to change text *) TQTXHeaderTitle = Class(TQTXLabel) private Procedure SetInheritedCaption(const aValue:String); protected procedure setCaption(const aValue:String);override; End; (* Header control, dynamically resizes and positions caption and button based on visibility. Otherwise identical to TW3HeaderControl *) TQTXHeaderBar = Class(TW3CustomControl) private FBackButton: TQTXBackButton; FNextButton: TQTXNextButton; FCaption: TQTXHeaderTitle; FMargin: Integer = 4; FFader: Boolean = false; Procedure HandleBackButtonVisibleChange(sender:TObject;aVisible:Boolean); Procedure HandleNextButtonVisibleChange(sender:TObject;aVisible:Boolean); protected Procedure setMargin(const aValue:Integer); Procedure Resize;override; Procedure InitializeObject;override; Procedure FinalizeObject;Override; public Property FadeTitle:Boolean read FFader write FFader; Property Margin:Integer read FMargin write setMargin; Property Title:TQTXHeaderTitle read FCaption; Property BackButton:TQTXBackButton read FBackButton; property NextButton:TQTXNextButton read FNextButton; End; implementation //############################################################################# // TQTXHeaderButton //############################################################################# (* This method simply exposes access to the inherited version of setVisible. Since inherited method cannot be called from anonymous event-handlers, we expose it here. *) Procedure TQTXHeaderButton.setInheritedVisible(const aValue:Boolean); Begin inherited setVisible(aValue); end; //############################################################################# // TQTXBackButton //############################################################################# procedure TQTXBackButton.setVisible(const aValue:Boolean); var mParent: TQTXHeaderBar; dx: Integer; Begin (* Make sure object is ready and that the button is injected into the DOM *) if ObjectReady and Handle.Ready and TQTXTools.getDocumentReady then Begin (* Make sure parent is valid *) if Parent<>NIL then Begin (* get parent by ref *) mParent:=TQTXHeaderBar(Parent); if aValue<>getVisible then begin case aValue of false: Begin if mParent.ObjectReady and mParent.Handle.Ready then Begin dx:=-Width; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} self.fxMoveTo(dx,top,CNT_ANIM_DELAY, procedure () begin setInheritedVisible(false); end); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end else setInheritedVisible(false); end; True: Begin setInheritedVisible(true); self.MoveTo(-Width, (mParent.ClientHeight div 2) - self.height div 2); if mParent.ObjectReady and mParent.Handle.Ready then {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () {$ENDIF} Begin self.fxMoveTo(mParent.margin, (mParent.ClientHeight div 2) - self.height div 2,CNT_ANIM_DELAY); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ELSE} end; {$ENDIF} end; end; if assigned(OnVisibleChange) and mParent.Handle.Ready then OnVisibleChange(self,aValue); end; end; end else inherited setVisible(aValue); end; //############################################################################# // TQTXNextButton //############################################################################# procedure TQTXNextButton.setVisible(const aValue:Boolean); var dy: Integer; dx: Integer; mParent: TQTXHeaderBar; Begin (* Make sure element is ready and inserted into the DOM *) if ObjectReady and TQTXTools.getDocumentReady and Handle.Ready then Begin (* make sure parent is valid *) if parent<>NIL then begin (* Make sure this represents a change in state *) if aValue<>getVisible then Begin (* cast parent to local variable *) mParent:=TQTXHeaderBar(Parent); case aValue of false: begin (* move button out to the right *) dy:=top; dx:=mParent.Width; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} self.fxMoveTo(dx,dy,CNT_ANIM_DELAY, procedure () begin setInheritedVisible(false); end); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; true: begin (* move button in to the left *) setInheritedVisible(true); dy:=top; dx:=(mParent.ClientWidth - mparent.margin) - self.Width; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} self.fxMoveTo(dx,dy,CNT_ANIM_DELAY); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; if assigned(OnVisibleChange) then OnVisibleChange(self,aValue); end; end; end else inherited setVisible(aValue); end; //############################################################################# // TQTXHeaderTitle //############################################################################# Procedure TQTXHeaderTitle.SetInheritedCaption(const aValue:String); Begin inherited setCaption(aValue); end; Procedure TQTXHeaderTitle.setCaption(const aValue:String); begin (* Make sure we can do this *) if ObjectReady and TQTXTools.getDocumentReady and Handle.Ready then Begin (* Check valid parent *) if Parent<>NIL then Begin (* Use fading at all? *) if TQTXHeaderBar(Parent).FadeTitle then Begin {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} self.fxFadeOut(CNT_ANIM_DELAY, procedure () Begin setInheritedCaption(aValue); self.fxFadeIn(CNT_ANIM_DELAY); end); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end else setInheritedCaption(aValue); end else inherited setCaption(aValue); end else inherited setCaption(aValue); end; //############################################################################# // TQTXHeaderBar //############################################################################# Procedure TQTXHeaderBar.InitializeObject; Begin inherited; StyleClass:='TW3HeaderControl'; FBackButton:=TQTXBackButton.Create(self); FBackButton.setInheritedVisible(false); FBackbutton.styleClass:='TW3ToolButton'; FBackbutton.Caption:='Back'; FBackbutton.Height:=28; FNextButton:=TQTXNextButton.Create(self); FNextButton.setInheritedVisible(false); FNextButton.styleClass:='TW3ToolButton'; FNextButton.Caption:='Next'; FNextButton.height:=28; FCaption:=TQTXHeaderTitle.Create(self); FCaption.Autosize:=False; FCaption.Caption:='Welcome'; //FCaption.handle.style['border']:='1px solid #444444'; //FCaption.handle.style['background-color']:='rgba(255,255,255,0.3)'; (* hook up events when element is injected in the DOM *) TQTXTools.ExecuteOnElementReady(Handle, procedure () Begin (* Use update mechanism, which forces an internal resize when sized flag is set *) beginUpdate; try FBackButton.OnVisibleChange:=HandleBackButtonVisibleChange; FNextButton.OnVisibleChange:=HandleNextButtonVisibleChange; setWasMoved; setWasSized; finally EndUpdate; end; end); end; Procedure TQTXHeaderBar.FinalizeObject; Begin FBackbutton.free; FNextButton.free; FCaption.free; inherited; end; Procedure TQTXHeaderBar.setMargin(const aValue:Integer); Begin if aValue<>FMargin then begin (* If the element is not ready, try again in 100 ms *) if ObjectReady and TQTXTools.getDocumentReady and Handle.Ready then Begin BeginUpdate; FMargin:=TInteger.EnsureRange(aValue,1,MAX_INT); setWasSized; endUpdate; end else w3_callback(procedure () begin setMargin(aValue); end,100); end; end; Procedure TQTXHeaderBar.HandleNextButtonVisibleChange (sender:TObject;aVisible:Boolean); var wd,dx: Integer; Begin case aVisible of false: begin wd:=clientwidth; dec(wd,FMargin); if FBackButton.Visible then dec(wd,FBackButton.width + FMargin); dx:=FMargin; if FBackButton.visible then inc(dx,FBackButton.Width + FMargin); if ObjectReady and Handle.Ready then Begin wd:=wd - FMargin; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FCaption.fxScaleTo(dx, (clientHeight div 2) - FCaption.Height div 2, wd, FCaption.height, CNT_ANIM_DELAY, NIL); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; true: Begin dx:=FMargin; if FBackButton.visible then inc(dx,FBackButton.Width + FMargin); wd:=ClientWidth - (2 * FMargin); if FBackButton.Visible then dec(wd,FBackButton.width); dec(wd,FNextButton.Width); dec(wd,FMargin * 2); {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FCaption.fxSizeTo(wd,FCaption.Height,CNT_ANIM_DELAY, procedure () Begin FCaption.fxMoveTo(dx, (clientHeight div 2) - FCaption.Height div 2, CNT_ANIM_DELAY); end); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; Resize; end; Procedure TQTXHeaderBar.HandleBackButtonVisibleChange (sender:TObject;aVisible:Boolean); var dx: Integer; wd: Integer; Begin case aVisible of false: begin {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FBackButton.fxMoveTo(-FBackButton.width, (clientheight div 2) - FBackButton.height div 2, CNT_ANIM_DELAY); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; true: Begin {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FBackButton.fxMoveTo(FMargin, (clientheight div 2) - FBackButton.height div 2, CNT_ANIM_DELAY); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; case aVisible of false: Begin wd:=ClientWidth - (FMargin * 2); if FNextButton.Visible then Begin dec(wd,FNextButton.Width); dec(wd,FMargin); end; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FCaption.fxScaleTo(Fmargin, (clientHeight div 2) - (FCaption.height div 2), wd,FCaption.Height,CNT_ANIM_DELAY,NIL); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; true: Begin dx:=FMargin + BackButton.Width + FMargin; wd:=ClientWidth - (FMargin * 2); dec(wd,FBackButton.width); if FNextButton.visible then Begin dec(wd,FNextButton.Width); dec(wd,FMargin * 2); end else dec(wd,FMargin); {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FCaption.fxScaleTo(dx, (clientHeight div 2) - (FCaption.height div 2), wd,FCaption.Height, CNT_ANIM_DELAY, NIL); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; end; Procedure TQTXHeaderBar.Resize; var dx: Integer; wd: Integer; Begin inherited; if FBackbutton.visible then FBackbutton.setbounds(FMargin, (clientheight div 2) - FBackButton.height div 2, FBackButton.width, FBackbutton.height); if FNextButton.visible then FNextButton.setBounds((clientwidth-FMargin)-FNextButton.width, (clientHeight div 2) - FNextButton.height div 2, FNextButton.width, FNextButton.Height); dx:=FMargin; if FBackButton.visible then inc(dx,FBackButton.Width + FMargin); wd:=ClientWidth - FMargin; if FBackButton.visible then dec(wd,FBackButton.Width + FMargin); if FNextButton.visible then begin dec(wd,FNextButton.width + FMargin); dec(wd,FMargin); end else dec(wd,FMargin); FCaption.SetBounds(dx, (clientHeight div 2) - (FCaption.height div 2), wd,FCaption.Height); end; end.
Casebook source
Updated
All code moved to the same repo. Casebook is now a part of QTXLibrary!
If you want to have a peek at the “casebook” skeleton app (http://lennartaasenden.magix.net/public/) then you can download the “R&D” source in zip format here.
Please be warned that this source will change drastically over the next weeks and that it’s undocumented. The first to be added will be a datasource so articles are truly dynamic, meaning the source of articles can either be a “live” webservice, a file stored on the server or a JSON service.
Also be sure you grab the QTX library here: https://code.google.com/p/qtxlibrary/
A few notes
Heavy use of callback’s and delays make for a more fluent and responsive interface. You will find plenty of this, especially in the event-handlers for buttons and form navigation.
Cloud effects turn themselves off when the present-form is not the login-form, but they can complete a cycle before that happens.
Dont be afraid to mess things up
Adding support for Font Awesome
This really made my day. I was busy hacking away at caseBook when I suddenly realized – I have no glyphs that fit the bill. So I googled CSS glyphs and voila – there was Font Awesome, 100% re-usable, CSS based glyphs. Ready to be used.
Since Smart Mobile Studio hides the HTML template for your project, I had to add a new function for adding a reference to external files. You will find it in qtxutils.pas (see google repository for QTXLibrary), and it goes a little something like this:
class function TQTXTools.addLinkToHead(const aRel,aHref:String):THandle; var mLink: THandle; Begin //REL: Can be &amp;quot;stylesheet&amp;quot; and many more values. // See http://www.w3schools.com/tags/att_link_rel.asp // for a list of all options asm @mLink = document.createElement('link'); (@mLink).href = @aHref; (@mLink).rel=@aRel; document.head.appendChild(@mLink); end; result:=mLink; end;
Next, I went into the project-unit, and added the following to TApplication.ApplicationStarting:
TQTXTools.addLinkToHead('stylesheet', 'http://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css');
And thats it! You can now use a plethora of CSS glyphs, including spinning cogs and all the whistles and bell’s used by popular high-end javascript websites.
And example would be, for instance, to get a home glyph + animated cog wheel (which rotates perfectly):
FPanel.handle.style['color']:='#cccccc'; FPanel.innerHTML:=#'<i class="fa fa-home fa-2x"></i> <i class="fa fa-user fa-2x"></i> <i class="fa fa-cog fa-spin fa-2x"></i>';
This really is a perfect example of just how easy it is to absorb already existing JavaScript and HTML5 libraries with Smart Mobile Studio. I could have gone with some hand painted 24×24 pixel png files — but why bother when someone has been kind enough to make such a fantastic library of scalable css based glyphs for the public?
And yes, I realize I just gave you a sneak-peek of caseBook.. It’s taking shape 🙂
Check out Font awesome here: http://fortawesome.github.io/Font-Awesome/
And as always, the QTXLibrary is located on google code, here: https://code.google.com/p/qtxlibrary/
Caching effects
Sort of discovered this by surprise. I was making the typical “mouse-over” effects for buttons, where they grow a few pixels when you hover over them. But, since only one webkit transformation can be applied to an element at any given time, I decided to use a callback to catch effects that overlap.
Well, it’s pretty cool! Move the mouse pointer in and out of the control very quickly to see what I mean. And naturally, the element never looses its original size. This is perfect for music vumeter’s — or perhaps a ripple effect that never loses track of the original size?
W3Button1.OnMouseEnter:=Procedure (sender:TObject; Shift: TShiftState; X, Y: Integer) Begin if not w3button1.fxBusy then w3Button1.fxScaleUp(4,0.2) else w3_callback( procedure () Begin w3Button1.OnMouseEnter(sender,shift,x,y); end, 100); end; w3button1.OnMouseExit:=procedure (sender:TObject; Shift: TShiftState; X, Y: Integer) Begin if not w3button1.fxBusy then w3Button1.fxScaleDown(4,0.2) else w3_callback( procedure () Begin w3Button1.onMouseExit(sender,shift,x,y); end, 100); end;
Oh, and remember to update the QTX svn library before trying it !
Updated
I decided to add this form of caching to the library itself. This completely removes the need for an effect-stack (linear “list” of effects to execute one after the other).
Well, everything is now in place to write some interesting, dynamic and awesome Smart Pascal visual components. I could be tempted to write a book on advanced Smart Mobile coding — interested?
Storing stuff inside the tag

User definable per-tag attributes
Another thing I learned today. As you probably understand by now, a Smart Visual Component consists of two parts. First, there is the javascript object which is what you compile from Object Pascal, secondly there is the HTML element which your control manages via code.
But what if you want to store some data which can be read/written without access to the JS code?
Let me introduce you to the problem. The effects library is capable of animating any TW3CustomControl based element, regardless of what basetype it implements (DIV, PRE, IFRAME etc..). But it does this via class helpers – so how can we store a boolean value informing us that an effect is active? We dont want to re-mold the RTL (at least not before the technology is rock solid) — so how can we attach data to an element without altering the virtual method table data?
Ta-Da! — HTML5 introduces something called Data Attributes (read more here: http://html5doctor.com/html5-custom-data-attributes/) . Basically, whenever you use attributes on a tag which begins with “data-“, the browser leaves them in place (ordinarily it would remove them or not include them in the DOM). This allows us to do stuff like below, without affecting javascript, the RTL or the DOM negatively in any way (!):
<DIV ID="w3Panel1" data-fxActive="true"> .. </DIV>
In the QTX library read/write access to data-attributes is introduced in the class TQTXAttrAccess (qtxutils.pas). So if you include this unit in your project, all TW3CustomControl based elements gain an extra property called ElementData of type TQTXAttrAccess. Think of it like an unlimited box of TComponent.Tag values 🙂
Effects without stylesheet punching
Unless you have been living under a rock you would know that the GPU can only be accessed via CSS. This means you have to write stylesheet code (which you can edit inside Smart Mobile Studio). But this ties you down a bit, because what if you want to re-use some effects in another app? You either have to copy the CSS and Pascal code over — or… you can author the effect CSS in smart pascal directly.
And that my friend is why you will find a class called TQTXStyleSheet in the qtxstyles.pas unit. It allows you to write cutting edge GPU effects that are portable between projects — and you can leave the default, model view stylesheet intact.
You are welcome!
QTXEffects unit for Smart Mobile Studio
A couple of days ago I published a small class-helper for Smart Mobile Studio which implemented a couple of basic, jQuery like, effects that you can apply directly to any control.
Since then I have spent 5 minutes implementing the “missing” effects. After all, jQuery have a couple of variations on the theme of fading elements. Since Smart Mobile Studio comes with several interesting effect objects out of the box, I decided to wrap all of them in a handy class helper.
Why a class helper?
Whenever you include a unit with a class helper in your forms (or your own units) whatever methods provided by the class helper, becomes available as a part of the target. In this case the target is TW3CustomControl, which means that you can now call the effect methods directly from any control – and they will be applied directly on the component.
How do I use it?
Simple, first include the effect unit (qtxEffects.pas) in your uses-list, then drop a couple of controls on your form – and call one of the functions. Like fadeOut(0.5) for instance. In that case whatever control you chose will fade out in a matter of 0,5 seconds.
If you drop a button on the main-form, double-click it and write the following in the event handler, then the button will gracefully fade into opacity as described above.
if w3button1.visible then w3button1.fadeout(0.5);
More effects
Since writing CSS3 based effects is clever stuff, I will try to expand the library once in a while when time allows. jQuery have a few methods for moving elements from/to using hardware accelerated CSS – flipping things around etc., which should be fairly easy to replicate. While not substantially important in business applications they have their uses.
Everyone loves a GUI that is alive with elements fading gracefully into view, and elements moving with elegance into position. This is tricky enough to achieve under ordinary Delphi – let alone HTML5 and JavaScript. But thankfully Smart Mobile Studio have a few tricks up it’s sleeve.
Here is the updated library — enjoy!
unit qtxEffects; //############################################################################# // // Unit: qtxEffects.pas // Author: Jon Lennart Aasenden // Company: Jon Lennart Aasenden LTD // Copyright: Copyright Jon Lennart Aasenden, all rights reserved // // About: This unit introduces a class helper for TW3CustomControl // which provides jQuery like "effects", such as fadeIn/out etc. // // Note: Simply add this unit to your uses-list, and all controls // based on TW3CustomControl will have the new methods. // //############################################################################# interface uses w3System, w3Components, w3Effects; type TQTXMoveAnimation = class(TW3TransitionAnimation) private FFromX: Integer; FFromY: Integer; FToX: Integer; FToY: Integer; protected function KeyFramesCSS: String; override; public Property FromX:Integer read FFromX write FFromX; Property FromY:Integer read FFromY write FFromY; Property ToX:Integer read FToX write FToX; Property ToY:Integer read FToY write FToY; end; TQTXEffectsHelper = Class helper for TW3CustomControl Procedure fxFadeOut(const Duration:Float); Procedure fxFadeIn(const Duration:Float); Procedure fxWarpOut(const Duration:Float); procedure fxWarpIn(const Duration:Float); Procedure fxZoomIn(const Duration:Float); Procedure fxZoomOut(const Duration:Float); Procedure fxMoveTo(const dx,dy:Integer; const Duration:Float); Procedure fxMoveBy(const dx,dy:Integer; const Duration:Float); Procedure fxMoveUp(const Duration:Float); Procedure fxMoveDown(const Duration:Float); End; implementation function TQTXMoveAnimation.KeyFramesCSS: String; Begin Result := Format(#" from { left: %dpx; top: %dpx; } to { left: %dpx; top: %dpx; }",[FFromX,FFromY,FToX,FToY]); end; //############################################################################ // TQTXEffectsHelper //############################################################################ Procedure TQTXEffectsHelper.fxMoveUp(const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TQTXMoveAnimation.Create; mEffect.duration:=Duration; TQTXMoveAnimation(mEffect).fromX:=self.left; TQTXMoveAnimation(mEffect).fromY:=self.top; TQTXMoveAnimation(mEffect).toX:=self.left; TQTXMoveAnimation(mEffect).toY:=0; TQTXMoveAnimation(mEffect).Timing:=atEaseInOut; mEffect.onAnimationEnds:=Procedure (sender:TObject) Begin self.top:=0; w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; mEffect.execute(self); end; Procedure TQTXEffectsHelper.fxMoveDown(const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TQTXMoveAnimation.Create; mEffect.duration:=Duration; TQTXMoveAnimation(mEffect).fromX:=self.left; TQTXMoveAnimation(mEffect).fromY:=self.top; TQTXMoveAnimation(mEffect).toX:=self.left; TQTXMoveAnimation(mEffect).toY:=TW3MovableControl(self.Parent).Height-Self.Height; TQTXMoveAnimation(mEffect).Timing:=atEaseInOut; mEffect.onAnimationEnds:=Procedure (sender:TObject) Begin self.top:=TW3MovableControl(self.Parent).Height-Self.Height;; w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; mEffect.execute(self); end; Procedure TQTXEffectsHelper.fxMoveBy(const dx,dy:Integer; const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TQTXMoveAnimation.Create; mEffect.duration:=Duration; TQTXMoveAnimation(mEffect).fromX:=self.left; TQTXMoveAnimation(mEffect).fromY:=self.top; TQTXMoveAnimation(mEffect).toX:=self.left + dx; TQTXMoveAnimation(mEffect).toY:=self.top + dy; TQTXMoveAnimation(mEffect).Timing:=atEaseInOut; mEffect.onAnimationEnds:=Procedure (sender:TObject) Begin self.left:=dx; self.top:=dy; w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; mEffect.execute(self); end; Procedure TQTXEffectsHelper.fxMoveTo(const dx,dy:Integer;const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TQTXMoveAnimation.Create; mEffect.duration:=Duration; TQTXMoveAnimation(mEffect).fromX:=self.left; TQTXMoveAnimation(mEffect).fromY:=self.top; TQTXMoveAnimation(mEffect).toX:=dx; TQTXMoveAnimation(mEffect).toY:=dy; TQTXMoveAnimation(mEffect).Timing:=atEaseInOut; mEffect.onAnimationEnds:=Procedure (sender:TObject) Begin self.left:=dx; self.top:=dy; w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; mEffect.execute(self); end; Procedure TQTXEffectsHelper.fxZoomIn(const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TW3ZoomInTransition.Create; mEffect.Duration:=Duration; mEffect.OnAnimationEnds:=Procedure (Sender:TObject) Begin w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; self.Visible:=true; mEffect.Execute(self); end; Procedure TQTXEffectsHelper.fxZoomOut(const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TW3ZoomOutTransition.Create; mEffect.Duration:=Duration; mEffect.OnAnimationEnds:=Procedure (Sender:TObject) Begin self.Visible:=false; w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; mEffect.Execute(self); end; Procedure TQTXEffectsHelper.fxWarpOut(const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TW3WarpOutTransition.Create; mEffect.Duration:=Duration; mEffect.OnAnimationEnds:=Procedure (Sender:TObject) Begin self.Visible:=false; w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; mEffect.Execute(self); end; procedure TQTXEffectsHelper.fxWarpIn(const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TW3WarpInTransition.Create; mEffect.Duration:=Duration; mEffect.OnAnimationEnds:=Procedure (Sender:TObject) Begin w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; self.Visible:=true; mEffect.Execute(self); end; Procedure TQTXEffectsHelper.fxFadeIn(const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TW3FadeSlideTransition.Create; TW3FadeSlideTransition(mEffect).fromOpacity:=0.0; TW3FadeSlideTransition(mEffect).toOpacity:=1.0; mEffect.Duration:=Duration; mEffect.OnAnimationEnds:=Procedure (Sender:TObject) Begin w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; self.Visible:=true; mEffect.Execute(self); end; Procedure TQTXEffectsHelper.fxFadeOut(const Duration:Float); var mEffect: TW3CustomAnimation; Begin mEffect:=TW3FadeSlideTransition.Create; TW3FadeSlideTransition(mEffect).fromOpacity:=1.0; TW3FadeSlideTransition(mEffect).toOpacity:=0.0; mEffect.Duration:=Duration; mEffect.OnAnimationEnds:=Procedure (Sender:TObject) Begin self.Visible:=False; w3_callback( Procedure () Begin TW3CustomAnimation(sender).free; end, 100); end; mEffect.Execute(self); end; end.
You must be logged in to post a comment.