Quantcast
Channel: Delphi Haven
Viewing all 62 articles
Browse latest View live

Inspecting the default ‘platform’ FMX styles in XE3 (if you’re interested in that sort of thing)

$
0
0

One of the more obvious changes in FMX between the XE2 and XE3 releases is the (much) better default styling on Windows in particular. To my eyes this makes FMX controls in XE3 look like custom drawn VCL ones that try to look native (e.g. TSpeedButton), which for me at least is ‘good enough’ in principle, notwithstanding the odd tweak that still needs to be made in practice.

The main technical reason for this improvement is the use of bitmaps rather than an all-vector approach. Going by the limited help and marketing information, you may think this involves a new FMX style format (the XE2 one was in essence just the old DFM format), but this is not true – rather, the bitmaps used by the new platform styles are just hosted in a TImage control inside the style structure. In order to avoid unnecessary duplication and the inefficient use of lots of small bitmaps, a new TSubImage component has also been added to allow each platform style to have one big bitmap that different style elements can then reference different parts of. While TSubImage itself is not registered onto the Tool Palette (perhaps an oversight?), you can find it present and correct in FMX.Objects.pas, and when you do, you’ll find there’s very little to it.

What might lead you astray in thinking there is more to things than that is the fact XE2′s VCL style editor has been renamed the ‘Bitmap Style Editor’ in XE3. However, the extra functionality behind the name change is an FMX style export facility. While this will probably prove a valuable feature, the program still cannot open even the native FMX style files (*.style) it itself has created. The (very) early market for third party FMX style editors like Mike Sutton’s MonkeyStyler is therefore still very much open!

Inspecting the XE3 platform styles

Annoyingly enough, unlike in XE2, the platform styles for FMX in XE3 are not directly distributed with the product. I say annoyingly, since the ability to easily inspect the details of a style can be very useful. For example, having used a TTreeView control with a TLabel embedded right-aligned in each node for additional text, I quickly discovered that the OS X style uses a different font colour for selected tree view items that my additional labels weren’t picking up. Wanting to identify this colour at runtime, I initially tried to use the IDE’s style editor to locate what properties I needed to look up. However, this proved a fool’s errand – quite apart from the fact the native FMX style editor seems to be missing half its proper functionality, there wasn’t the OS X style file around to load into it in the first place!

Nonetheless, on browsing the FMX source folder I quickly came across the FMX.Platform.Win.rc and FMX.Platform.Mac.rc resource scripts. While the style files these reference are not shipped, the scripts clearly identify the resource names used (win7style and win8style for the Windows platform styles, and lionstyle and lion2xstyle for the OS X ones); and on closer inspection, the style resources themselves proved to be the raw *.style files, saved in the binary DFM format, and prepended with a 13 byte header. Putting all this together, I quickly wrote the following program to extract human-readable *.style files:

program Project1;

{$R *.res}

{$R 'C:\Program Files\Embarcadero\RAD Studio\10.0\lib\win32\release\FMX.Platform.Win.res'}
{$R 'C:\Program Files\Embarcadero\RAD Studio\10.0\lib\osx32\release\FMX.Platform.Mac.res'}

uses
  System.Types, System.SysUtils, System.Classes;

procedure ExtractStyle(const ResName, DestFileName: string);
var
  Input: TResourceStream;
  Output: TFileStream;
begin
  Output := nil;
  Input := TResourceStream.Create(HInstance, ResName, RT_RCDATA);
  try
    Input.Seek(13, soCurrent);
    Output := TFileStream.Create(DestFileName, fmCreate);
    ObjectBinaryToText(Input, Output);
  finally
    Input.Free;
    Output.Free;
  end;
end;

begin
  ExtractStyle('win7style', 'C:\Users\CCR\Documents\Win7.style');
  ExtractStyle('win8style', 'C:\Users\CCR\Documents\Win8.style');
  ExtractStyle('lionstyle', 'C:\Users\CCR\Documents\Lion.style');
  ExtractStyle('lion2xstyle', 'C:\Users\CCR\Documents\Lion2x.style');
end.

Adjust the paths as appropriate, run the application, and you’re done.



Cross platform FMX/XE3 demos for CCR Exif

$
0
0

I’ve just added a set of FMX demos in the SVN trunk for my image metadata reading/writing library, CCR Exif. These are ports of the existing VCL demos (minus the resaving tester). In alphabetical order, they number the following:

  • ExifList displays the Exif tags from an image, Exif being the metadata format most cameras use.
  • ExifTimeShifter shifts the Exif date/times for one or more images by a specified number of minutes.
  • IPTCEditor edits the IPTC data held in an image. IPTC is the metadata format Photoshop used back in the 1990s, and makes for a slightly lower-tech alternative to Exif.
  • JpegDump displays information from a JPEG file’s header, providing a lower-level view of its metadata tags than the other demos.
  • Screenshooter takes a screenshot, applies a few tags, and saves to a new JPEG file.
  • XMPBrowser displays XMP metadata in a tree view, XMP being a newer, XML-based metadata format.

While the FMX demos (which require XE3 by the way) mostly hew to the line set by their VCL forebears, I began with a blank slate in each case. This I did because I wanted certain UI details to follow the target platform (i.e., Windows or OS X), and in so doing, see what FMX did to help, or at least, see how far it wouldn’t get in the way.

Screenshooter and ExifTimeShift

The simplest demo to port was Screenshooter, since the FMX version has almost exactly the same UI for both platforms. The only difference is the fact the Mac version has the regular Mac menu bar; however, the menu items on it are just the standard ones that FMX in XE3 set up for you automatically:

Similar between platforms too is ExifTimeShift:

In this case there are some subtle differences though:

  • I hide a few toolbar buttons on OS X because toolbars on a Mac just tend to have less items than their Windows equivalents.
  • The Preferences/Options dialog is non-modal on OS X, with no explicit OK/Cancel buttons and changes applied immediately. (I leave aside whether this makes for a better UI – in fact, I think I prefer the Windows model myself – but that is the contemporary Apple style.)
  • On both platforms, double clicking a listed file displays the image. On Windows the image window gets its own taskbar button; on OS X it is full screen-enabled – you can see this in the screenshot with the double arrow icon on the title bar – and listed under the application’s Window menu. While I had to implement these things manually, it wasn’t hard, though I’d admit the Windows part requires more work (the FMX developers are unfortunately obsessed with a Win 3.x/VCL-style ‘main form’ concept that isn’t technically native even on Windows).
  • The file date/time preference is saved to the Registry on Windows and the application’s preferences file on OS X. Since I didn’t want to introduce dependencies, the latter is done by direct API calls, though in practice I would use my own Mac Preferences API wrapper I blogged about previously.

ExifList, IPTCEditor and XMPBrowser

The main platform difference between the remaining demos is much more ‘in your face’: whereas on Windows the applications are SDI, on OS X they follow the basics of the Mac document model. As such, if you run ExifList, IPTCEdit or XMPBrowser on OS X without passing the application a file first, it opens with no window showing. This follows what Preview does:

Further, whereas the same instance of the Mac version can have multiple images open, each with their own window (and enumerable using the system-standard Cmd+` shortcut), you need to open the Windows version several times to have it load several images at the same time:

Implementation

Each demo is essentially single source, and structured around my preferred VCL UI model. In this, the document form’s OnClick code is all centralised into action lists located on a data module separate from the form itself, with any necessary communication between the actions and the form being performed via interface types. A unit shared between all the demos (CCR.FMX.Demos.pas) then contains all the platform-specific UI-related code, and manages the differences between the different document models (no form is auto-created in the traditional Delphi fashion, and the document form class itself is looked up using RTTI). Alas, but that unit also implements certain crucial bug fixes for FMX itself, though at least the new IFMXxxxService system (see my previous-but-one post) made that bareable. I’ll probably blog about it in more detail later.


FMX in XE3 – good things and bad things

$
0
0

An easy way to summarise the good things about FMX in XE3 is to say it is much better than FMX in XE2; an easy way to summarise the bad things is to say it couldn’t have been any worse. Put another way, the promised regular XE2 updates for FMX turned out to be (a) less then regular, particularly after the first couple and (b) disappointingly slight; even so, browsing the source suggests a big reason for this was that the XE2 code was forked early on, with anyone competent devoting their time on the fork rather than the original.

Speaking in more specific terms, here’s some good things I’ve found about FMX in XE3:

  • The default Windows Vista/7 styling is pretty good, indeed good enough in my view. It’s at the level of custom drawn VCL controls (e.g. TSpeedButton) that try to look and behave ‘native’.
  • The awful walking-up-the-list thing I blogged about a few months ago has been largely fixed.
  • Actions are in.
  • Less a big deal than actions (at least for me), but anchors and a subset of the VCL’s touch and gesture support are in too.
  • TTabControl (equivalent to TPageControl in the VCL) actually works at design time. This is important if you are as tab-dependant like me…
  • It is possible to target OS X without a ‘main form’ concept being in effect, which is crucial to implement the basics of the Mac ‘document model’ (what I sometimes call myself ‘Mac-style MDI’).
  • The inherent flexibility of designing a UI using FMX compared to the VCL is still there.
  • The refactoring done for XE3 was largely for the better.

Nonetheless, even on its own terms, ‘FM2′ really needed a few more months. The bad things I’ve found include the following:

  • The menu code (crucial on OS X especially given the Mac menu bar) remains atrocious, being essentially the same as in XE2 -
    • Change a menu item’s state, and the whole menu bar is rebuilt (or would be – bugs and oversights mean nothing happens half the time).
    • TMainMenu is tied to a particular form, which doesn’t make sense on OS X.
    • Far more code than is actually necessary to do it right is used. I am inclined to say the person who wrote it is incompetent, though whoever authorised its inclusion and then failed to prioritise its fixing is just as bad.
  • Dancing text on Windows remains. This is a showstopper. What more can be said?
  • Too many properties and methods can return nil instead of raising an exception. A prime example of this is the Children property of TFmxObject when the object has no children. In that particular case, the cause is the internal FChildren list being nil until the first child is added, then freed and nil’ed when the final child is removed. This coding style is used elsewhere too, and I can only understand it as a ‘cargo cult’ practice picked up from reading the VCL source. In 2012 this only complicates the code unnecessarily however – truly, just create the list in the parent object’s constructor, and free it in the parent object’s destructor.
  • Still on the Children property, this directly exposes the internal list object. This is an error, because it allows client code to call SomeControl.Children.Add(NewChild) and therefore bypass the (virtual) AddObject method that remains the ‘proper’ way to add a child. Same with Delete/Remove and RemoveChild. (Before any says ‘but that’s just one property’ – TFmxObject is, as its name implies, a core class in the framework. Write it sloppily, and doubt arises about the quality of the framework generally.)
  • While the OS X side (well, most of it – see the next point) doesn’t enforce a ‘main form’ concept, the Windows side does. This is nothing to do with ‘native Windows behaviour’, but an ancient VCL-ism.
  • The small set of standard actions provided are unusable on OS X because they require a ‘main form’ to exist. They also have odd inefficiencies built into them.
  • The action code generally is over-fussy in my view, erroneously copying complications from the VCL code. I’m intending to blog about this separately.
  • Talking about actions, if you assign an action from a data module to a control, the IDE will sliently lose the association at a later point, just like in the VCL. Urgh!
  • I’m not sure about aspects of the default OS X styles. This isn’t necessarily EMBT’s fault – e.g., the ‘native’ equivalent of TTabControl is probably NSTabView, however NSTabView (a) puts has its ‘tabs’ centred not left-aligned and (b) isn’t actually used much by ‘native’ OS X applications. As it stands, the default OS X style for TTabControl takes its colours from NSTabView, but just looks weird because the sizing and positioning is different. Imitating the tab control styling in Safari or Firefox might be a better idea.
  • Efficient string interop on OS X appears to have been sacrificed in favour of cross compiling the FMX source using the upcoming ‘next generation compiler’ for iOS. To be clear, in your own code efficient string interop is still possible (both Delphi and Cocoa use UTF-16 internally) – it’s just that the FMX source deliberately uses clunky UTF-16 -> UTF-8 -> UTF-16 roundtrips via a ‘marshaller’.

My thoughts overall? If XE3 marked the first release of FMX, I would be very positive, notwithstanding the fact my list of ‘bad things’ is longer than the list of ‘good things’. In the case of the menu and action issues, for example, it is possible to work around them with a combination of interposer classes and custom IFMXxxxService implementations (for details, see this unit of mine). However, the happenings of a year ago are not just ‘water under the bridge’, and moreover, the fact FMX hasn’t been dogfooded even for the Platform Assistant installer (Java is still used instead!) doesn’t inspire confidence. The worry, quite simply, is that what remains to be fixed beyond straightforward bugs will now never be fixed.


Potential XE3 gotcha – dodgy old code vs. new TStream overloads

$
0
0

In XE3, TStream has acquired a number of new helper methods for Read and Write. Mostly these take the form of strongly-typed versions of Read, Write, ReadBuffer and WriteBuffer, called ReadData, WriteData, ReadBufferData and WriteBufferData respectively. These relieve you of the need to use SizeOf:

var
  Flag: Boolean;
  MagicNum: Integer;
begin
  Stream.ReadBufferData(Flag);
  if Flag then Stream.ReadBufferData(MagicNum);

I think adding these methods was a small but good idea, and a much better one than the TBinaryReader/TBinaryWriter classes that were pointlessly added in XE (cf. IOUtils, TStringBuilder, IEnumerable… ever get the feeling someone on the development team just never got over Big Bad EMBT Management killing off Delphi.NET?). It’s annoying whoever wrote the code hasn’t heard of static arrays, but the interface is fine.

However, there are also new overloads for Read, Write, ReadBuffer and WriteBuffer themselves. These take a TBytes parameter rather than an untyped buffer, and are presumably done for compatibility with the already-announced new compiler for targeting iOS and Android:

  function Read(var Buffer: TBytes; Count: Longint): Longint; overload;
  function Read(var Buffer: TBytes; Offset, Count: Longint): Longint; overload;
  function Write(const Buffer: TBytes; Count: Longint): Longint; overload;
  function Write(const Buffer: TBytes; Offset, Count: Longint): Longint; overload;
  procedure ReadBuffer(var Buffer: TBytes; Count: Longint); overload;
  procedure ReadBuffer(var Buffer: TBytes; Offset, Count: Longint); overload;
  procedure WriteBuffer(Buffer: TBytes; Count: Longint); overload;
  procedure WriteBuffer(Buffer: TBytes; Offset, Count: Longint); overload;

As shown by this QC report, the new overloads (and in particular, the ones without an additional Offset parameter) will alas be problematic though if you have old code that writes pointers on a stream on the understanding the addresses should be written.

Given pointer addresses are transient, this is in principle an odd thing to do. For myself, failing to dereference a pointer when calling Read- or WriteBuffer has caused infuriating crashes on more than one occasion! However, if you needed a list of integers in the past and lazily used a raw classic TList for the task, casting between Pointer and Integer as necessary, the code will need to be rewritten. The following, for example, will not work in XE3:

uses
  System.SysUtils, System.Classes;

var
  I: Integer;
  IntList: TList;
  Stream: TMemoryStream;
begin
  Stream := nil;
  IntList := TList.Create;
  try
    IntList.Add(Pointer(11));
    IntList.Add(Pointer(22));
    IntList.Add(Pointer(33));
    Stream := TMemoryStream.Create;
    //save list to the stream
    for I := 0 to IntList.Count - 1 do
      Stream.WriteBuffer(IntList.List[I], SizeOf(Integer));
    //clear the list before adding the items back from the stream
    IntList.Clear;
    Stream.Position := 0;
    while Stream.Read(I, SizeOf(Integer)) = SizeOf(Integer) do
      IntList.Add(Pointer(I));
    //show 'em
    for I := 0 to IntList.Count - 1 do
      WriteLn(Integer(IntList[I]));
  finally
    IntList.Free;
    Stream.Free;
  end;
end.

The problem here is that the when a value typed to Pointer is passed to an overloaded method that takes either an untyped paramater or a dynamic array, the dynamic array will be picked. This then causes an access violation, since the pointer being passed here isn’t actually a dynamic array of Byte – it’s an integer hard cast to a Pointer.

Now, perhaps there shouldn’t be type assignability here (for myself, I’d prefer only being able to assign a typed pointer of the appropriate sub-type, e.g. PByte to TBytes, PString to dynamic array of string and so on), however this has always been the case IIRC ever since dynamic arrays were added in D4. Morevoer, the proper way to do things before generics (which in practical terms meant until the generic TList actually worked properly) would have been to encapsulate TList, or at the very least hide the Pointer access members with Integer ones, a la Contnrs.pas’ TObjectList.

Because of these two points I think the closing of the QC report correct, notwithstanding the fact the narrative is wacko (‘test case error’? Er, no. XE3 has ‘fixed’ earlier behaviour? Er, no again – both old and new behaviours work correctly, and both stem from identical compiler semantics, they just conflict in a particular sort of case). Nonetheless, the overloads are still something to be wary of when recompiling old code.


FMX tip – default style lookup names

$
0
0

This one was driving me up the wall – I’m working on (or rather trying to work on) an FMX application, one form in which loads a tree view dynamically. Since each node concerns a certain sort of data object, I wrote a small custom descendant of TTreeViewItem to add a property of the relevant type and a couple of helper methods. So far so good. After that I did a bit of refactoring, separating out the descendant into a custom base class and two child classes, and adding bits and bobs. Following this I reran the program… and found the tree view text had disappeared. Argh!!! Progressively commenting out the new code got me nowhere, until tracing back my step of splitting out the custom descendant into three got me the answer – the now-grandchildren of TTreeViewItem were style-less!

If you check out the FMX source, you’ll see that the default style lookup for an object is its class name, minus the leading ‘T’ and with a ‘style’ prefix. This can be customised if you wish – in the form designer, it’s the StyleLookup property that gets set when you choose ‘Custom Style’ from the designer’s popup menu. If you customise it with the name of a style element that is not part of the active style (e.g., you used the ‘Custom Style’ command in the designer, only to delete the generated style book afterwards), then the default lookup based on the class name is used; and if that doesn’t name a valid style element, then the default style lookup for the parent class is used. If that still results in failure, then the control is just left style-less, and therefore empty at runtime unless the appearance is hardcoded (which is bad form for a FMX control). In my view, if you’re going to start walking up the class hierarchy for a valid style lookup, then you might as well continue to walk until you find one. In fact, more strongly, it surely breaks basic OOP principles for the act of merely splitting out a base class from a class to have the effect of altering (and in this case, breaking) its behaviour.

Regardless, my next step was to assign the StyleLookup property to ‘treeviewitemstyle’ in my custom base class’ constructor. This fixed the original problem of text not appearing… but caused a new issue of check boxes showing even though the tree view’s ShowCheckboxes property remained False! Tracing into the code, I found assigning the StyleLookup property causes a control to actually load its style right there and then; in my custom class’ case, this meant the style was being loaded before the control’s parentage had been established, which was undoubtedly too early (while the default value of the ShowCheckboxes property is False, the default visibility of the check box element of a tree view item style is True). Nonetheless, the fix was easy: assign not the StyleLookup property, but the FStyleLookup protected field:

type
  TMyNodeBase = class(TTreeViewItem)
  public
    constructor Create(const AOwner: TComponent; const AData: TDataObject); reintroduce;
  end;

  TMyRedNode = class(TMyNodeBase)
  //...
  end;

  TMyBlueNode = class(TMyNodeBase)
  //...
  end;

constructor TMyNodeBase.Create(const AOwner: TComponent; const AData: TDataObject);
begin
  inherited Create(AOwner);
  FStyleLookup := 'treeviewitemstyle'; //prevent descendants being style-less
  FData := AData;
end;

Quick FMX tip – TBitmap.CopyFromBitmap is not synonymous with TBitmap.Assign

$
0
0

Quick FMX tip – if you are calling the CopyFromBitmap method of TBitmap and wondering why it isn’t working (cf. the first issue reported here), it’s probably because you are attempting to write to an empty bitmap. In other words, like the CopyRect method in the VCL, CopyFromBitmap does not resize the destination bitmap, so the latter needs to be big enough beforehand if you want all the source pixels to be copied over. Nonetheless, if you want to resize the destination bitmap as well, just call Assign rather than CopyFromBitmap:

DestImage.Bitmap.Assign(SourceBitmap);

Retrieving the application’s version string

$
0
0

If you need to retrieve at runtime your application’s version number, you’re targeting Windows, and you don’t mind just having the major and minor numbers, then SysUtils has a handy GetFileVersion function for the task:

function GetAppVersionStr: string;
var
  Rec: LongRec;
begin
  Rec := LongRec(GetFileVersion(ParamStr(0)));
  Result := Format('%d.%d', [Rec.Hi, Rec.Lo])
end;

If you want the ‘release’ and ‘build’ numbers as well, you’ll need to drop down to the Windows API:

function GetAppVersionStr: string;
var
  Exe: string;
  Size, Handle: DWORD;
  Buffer: TBytes;
  FixedPtr: PVSFixedFileInfo;
begin
  Exe := ParamStr(0);
  Size := GetFileVersionInfoSize(PChar(Exe), Handle);
  if Size = 0 then
    RaiseLastOSError;
  SetLength(Buffer, Size);
  if not GetFileVersionInfo(PChar(Exe), Handle, Size, Buffer) then
    RaiseLastOSError;
  if not VerQueryValue(Buffer, '\', Pointer(FixedPtr), Size) then
    RaiseLastOSError;
  Result := Format('%d.%d.%d.%d',
    [LongRec(FixedPtr.dwFileVersionMS).Hi,  //major
     LongRec(FixedPtr.dwFileVersionMS).Lo,  //minor
     LongRec(FixedPtr.dwFileVersionLS).Hi,  //release
     LongRec(FixedPtr.dwFileVersionLS).Lo]) //build
end;

If targeting OS X then GetFileVersion isn’t available, so you need to drop down to the OS API again. This time the code is much simpler however:

uses Macapi.CoreFoundation;

function GetAppVersionStr: string;
var
  CFStr: CFStringRef;
  Range: CFRange;
begin
  CFStr := CFBundleGetValueForInfoDictionaryKey(
    CFBundleGetMainBundle, kCFBundleVersionKey);
  Range.location := 0;
  Range.length := CFStringGetLength(CFStr);
  SetLength(Result, Range.length);
  CFStringGetCharacters(CFStr, Range, PChar(Result));
end;

This uses Core Foundation; you could use Cocoa instead, but when an equivalent ‘Core’ API is available, it’s usually the better choice in Delphi given it can be called directly, and with code almost identical to the Objective-C samples available online and elsewhere.


Dragging an FMX list box item to a grid without superfluous highlighting

$
0
0

This a somewhat specific problem, but anyhow, I have an FMX application I’m writing in which I was wanting to be able to drag a list box item to a grid. Now, the FMX TListBox has an AutoDrag property to allow its items to be dragged, and FMX controls – like VCL ones – have OnDragOver and OnDragDrop events in which the former allows you to say whether the control will accept a dragged object and the latter allows the control to actually accept it. While a TListBox’s AutoDrag property is geared to allowing the user to reorder its items internally, setting it to True allows the user to drag an item outside the list box as well.

All in all then, the core functionality I required was implemented by the framework. However, by default, the behaviour wasn’t quite what I wanted:

  • While making it no effort to have a list box item draggable outside its parent was great, I didn’t want the accompanying reordering visuals when the dragged item was still within the list box bounds.
  • Most FMX controls visually indicate that they will accept an object being dragged over them regardless of any OnDragOver handler being set. So, if the form has a list box, a grid and a panel, and a list box item is being dragged, all three controls, together with the remaining list box items, will appear to accept the item as the item is dragged over it. (In the list box’s case, the entire list box is highlighted if you aren’t hovering over a specific item.)
  • In the case of a grid, each column header will indicate acceptance individually as well. Worse, they also support dragging and dropping between themselves to reorder and dragging to somewhere outside the grid with no obvious way to turn either ability off! Speaking for myself, this means I find it pretty easy to reorder the columns of a FMX grid without intending to. Moreover, the fact you can drag a grid header outside the grid is just confusing:

Panel drag highlight

In looking to fix these issues, the first thing I discovered was that the annoying visual acceptance effect is controlled by the EnableDragHighlight property, as introduced with public visibility (and a default setting of True!) in TControl, and subsequently published by most descendants. This property has zero connection to the OnDragOver event, which means that regardless of what you do in an OnDragOver handler, or indeed whether you have a handler for that event at all, by default, a control will visually indicate it accepts anything and everything being dragged over it. Nonetheless, if you have a number of controls that will never accept a dragged object, the easy fix is just to select them all at design time and toggle their EnableDragHighlight property to False. In my case that meant selecting everything on the form, deselecting the popup menus (which don’t have an EnableDragHighlight property), toggling the property in the Object Inspector, then individually putting the property back on a case-by-case basis.

This was also the fix for removing the reordering visuals from the list box, since TListBoxItem is one of the TControl descendants that publish EnableDragHighlight. In principle, you should still need to remove the actual reordering if an internal drag and drop happens accidentally. However, due to an oversight in the FMX source, the actual reordering doesn’t happen – the items are repositioned in the internal list of child objects, but their positions on screen are not switched similarly. Assuming that bug will one day be fixed though, the way to prevent any reordering is to handle the list box’s OnDragChange event and set the Allow parameter to False:

procedure TForm1.ListBox1DragChange(SourceItem,
  DestItem: TListBoxItem; var Allow: Boolean);
begin
  Allow := False;
end;

So, that left the grid headers… oh, and the listbox itself, since while setting EnableDragHighlight to False in the Object Inspector worked for individual list box items, it stubbornly wasn’t for their parent control:

List box drag highlight

Looking at the source, the reason stemmed from the AutoDrag property setter forceably setting EnableDragHighlight to True. Why? Who knows, but anyhow, where EnableDragHighlight is published by an ancestor class, TScrollBox, AllowDrag is published by TCustomListBox. Ergo, the persisted value of AllowDrag will be read back in after the persisted value of EnableDragHighlight, causing the former to override the latter. The fix is therefore to set the list box’s EnableDragHighlight property to False at runtime in (say) the form’s OnCreate handler:

procedure TForm1.FormCreate(Sender: TObject);
begin
  ListBox1.EnableDragHighlight := False;
end;

This just left getting rid of the grid column headers’ default draggability. Since a FMX grid’s header is defined by its style, the place to tweak the settings of it is in a handler for the grid’s OnApplyStyleLookup event. In terms of what needed to be done there, initially I thought of EnableDragHighlight again, but that wouldn’t solve the problem of the header items being themselves draggable. I then realised that the columns themselves were child controls of the grid just as much as the header, and they didn’t have independent draggability (so to speak). The reason for that, it became clear, was because their HitTest properties are set to False by the grid. So, the fix for the header was just to set both the header and its own child control’s HitTest properties to False likewise:

procedure TForm1.Grid1ApplyStyleLookup(Sender: TObject);
var
  StyleObj: TFmxObject;
  ChildObj: TControl;
begin
  StyleObj := (Sender as TCustomGrid).FindStyleResource('header');
  if StyleObj is THeader then
  begin
    THeader(StyleObj).HitTest := False;
    for ChildObj in THeader(StyleObj).Controls do
      ChildObj.HitTest := False;
  end;
end;

This of course means the user cannot click on a header item. If you want to have that, assign the header items’ OnClick handlers as desired, set their DragMode properties to TDragMode.dmManual, and their EnableDragHighlight properties to False:

procedure TForm1.Grid1ApplyStyleLookup(Sender: TObject);
var
  StyleObj: TFmxObject;
  ChildObj: TControl;
begin
  StyleObj := (Sender as TCustomGrid).FindStyleResource('header');
  if StyleObj is THeader then
  begin
    THeader(StyleObj).HitTest := False;
    for ChildObj in THeader(StyleObj).Controls do
    begin
      ChildObj.DragMode := TDragMode.dmManual;
      ChildObj.EnableDragHighlight := False;
      if ChildObj is THeaderItem then
        THeaderItem(ChildObj).OnClick := MyHeaderItemClick;
    end;
  end;
end;

(As an aside, you will frequently find the FMX source doing an Assigned check before using the ‘is’ operator – if the above code were written by an FMX author or maintainer, you would therefore expect to see ‘if Assigned(StyleObj) and (StyleObj is THeader) then’ rather than just ‘if StyleObj is THeader then’. This is completely unnecessary however, since the ‘is’ operator checks for nil itself.)



FMX OnCloseQuery and OnClose woes on OS X – fixing QC 111362 (+ another bug)

$
0
0

If you’ve attempted to use FMX to write an application targeting OS X, you may well have come across CloseQuery bugs – for me it’s generally been any dialog shown by a form’s OnCloseQuery handler having a tendancy to show twice, though you can also get the opposite problem, of the OnCloseQuery handler not being shown at all. QC 111362 (not by me) provides a clear test case of both: in the first instance, the user answering ‘yes’ to the save changes prompt leads to the prompt showing again, and in the second, assigning the ModalResult property of a button leads to OnCloseQuery not being raised at all when the button is clicked.

Having been bugging me for a while, I finally decided to investigate the problems properly. First up was the showing-twice issue. At first I thought it must be dialog-related, but it wasn’t:

  • As in the VCL, the FMX TForm has CloseQuery and Close methods, with the implementation of the latter calling the former. (Unlike in the VCL, FMX does not abstract out calling the OnClose event into a DoClose method. Instead, it is called directly inside Close. But I digress)
  • At the platform-specific level, FMX.Platform.pas calls CloseQuery in response to the windowShouldClose: message, which is good, and Close in response to windowWillClose:… which is bad. Why? Because as its name implies, by the time windowWillClose: is sent, the window is closing, not asking whether it should close. When the user clicks the red traffic light, the message order is therefore windowShouldClose:, and if that returns True, windowWillClose:. When OnCloseQuery has been handled without setting CanClose to False, this means it gets called a second time. Doh!
  • Instead of calling Close, the windowWillClose: handler should just invoke any assigned OnClose handler, if that. If a DoClose method had existed, perhaps the author would have at least thought whether Close rather DoClose is really appropriate…

Now, I say that windowWillClose: should call OnClose ‘if that’ because OnClose itself acts as a ‘are you really, really sure you want the form to close?’ event. This is because if a handler sets the Action var parameter to TCloseAction.caNone, in principle, the form should not now close:

procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if MessageDlg('Are you SURE you want the form to close?',
    TMsgDlgType.mtConfirmation, mbYesNo) <> mrYes then
    Action := TCloseAction.caNone;
end;

Predictably enough though, such code doesn’t work when targeting OS X, assuming Close is called because of the user attempting to close the form rather than the form being closed programmatically. More exactly, setting Action to anything beyond the default (caHide) doesn’t have any affect, because Cocoa has already decided what will happen. To make even OnClose work correctly, then, almost the entire logic needs to be placed in the windowShouldClose: handler, and the windowWillClose: handler left relatively bare.

As for the bug of OnCloseQuery not being raised at all, the problem there is specific to a modal form being closed due to its ModalResult property being set, either directly or via a button. Looking at the source, the CloseModal call inside TPlatformCocoa.ShowWindowModal looked suspicious, and sure enough, it was the problem:

  • As before, the FMX TForm mimics its VCL forebear in having a CloseModal method. In the VCL (and FMX/Windows), this is is called inside the modal message loop once a change in the form’s ModalResult property has been detected. CloseModal itself then calls CloseQuery to give the client code the chance to cancel the closure; if CloseQuery returns False, the ModalResult property is reset to mrNone (0). Back in the modal message loop, the ModalResult property is then checked a second time to allow any reset to take effect.
  • The author of TPlatformCocoa.ShowWindowModal misunderstood the purpose of CloseModal, apparently assuming it is some sort of notification method rather than a ‘last chance to cancel’ one. As such, CloseModal is called in the wrong place (too late), and ModalResult not rechecked.

Here’s how to fix these various bugs:

  • Take a copy of FMX.Platform.Mac.pas, and put it in your application’s main source directory (you can also explicitly add it to the project if you want).
  • Open the copy and head to the TFMXWindow.windowShouldClose method implementation. Change it look like this:

function TFMXWindow.windowShouldClose(Sender: Pointer): Boolean;
var
  Action: TCloseAction;                                  //!!!CCR
begin
  Result := False;
  if Application = nil then
    Exit;
  if Application.Terminated then
    Exit;
  try
    Result := Wnd.CloseQuery;
    if Result and Assigned(Wnd.OnClose) then             //!!!CCR
    begin                                                //!!!CCR
      Action := TCloseAction.caHide;                     //!!!CCR
      Wnd.OnClose(Wnd, Action);                          //!!!CCR
      if Action = TCloseAction.caMinimize then           //!!!CCR
      begin                                              //!!!CCR
        Result := False;                                 //!!!CCR
        Wnd.WindowState := TWindowState.wsMinimized;     //!!!CCR
      end                                                //!!!CCR
      else                                               //!!!CCR
        if (Application <> nil) and                      //!!!CCR
           (Wnd = Application.MainForm) then             //!!!CCR
          Result := (Action in [TCloseAction.caHide,     //!!!CCR
            TCloseAction.caFree])                        //!!!CCR
        else                                             //!!!CCR
        begin                                            //!!!CCR
          Result := (Action = TCloseAction.caHide);      //!!!CCR
          if Action = TCloseAction.caFree then Wnd.Release;
        end;                                             //!!!CCR
    end;
  except
    Application.HandleException(Self);
  end;
end;

The next method down is probably TFMXWindow.windowWillClose; in it, delete everything, before adding the following in its place:

procedure TFMXWindow.windowWillClose(
  notification: NSNotification);
begin
  if (Application <> nil) and (Application.MainForm = Wnd) then
    Application.Terminate;
end;

The addition preserves the ‘main form’ semantics we implicitly remove by not calling Close (which would otherwise enforce them).

The revised windowShouldClose and windowWillClose methods fix the double shows and OnClose not working properly; to fix the no-shows, head next to the implementation of TPlatformCocoa.ShowWindowModal. Find the call the AForm.CloseModal, and comment it out (it’s in the wrong place). Finally, find the check for AForm.ModalResult not being 0, and amend the code to look like this:

          if AForm.ModalResult <> 0 then
          begin
            AForm.CloseModal;                     //!!!CCR
            if AForm.ModalResult <> 0 then        //!!!CCR
            begin                                 //!!!CCR
              NSApp.stopModal;
              Continue;
            end;                                  //!!!CCR
          end;

To take effect, you may need to disable debug DCUs to ensure the revised unit gets picked up (Project|Options, Delphi Compiler > Compiling, uncheck ‘Use debug DCUs’ – you’ll only need to do this for the OS X debug configuration).

As a final point, please keep in mind I do not encourage invoking event handlers directly, as the code above does – the fact the FMX source doesn’t bother with dedicated event-calling methods half the time says nothing about the validity of such practice. The only reason I do it above is because I wish to avoid interface changes, and therfore, DCU incompatibilities.


FMX/OS X tip: enabling compiler optimisations to avoid weird startup crashes

$
0
0

I wasn’t able to trace the precise change that did it, but at a certain point I had the peculiar problem of an OS X debug build crashing at startup, and hanging the IDE if I attempted to run it in the debugger. Switching to the ‘release’ build configuration allowed it to run, but when you’re working with a framework that is so, er, work-in-progress as FMX, not being able to run an application in the debugger for a certain target platform is rather debilitating.

Seeking to investigate the problem, I created a copy of the project and progressively removed units until it ran properly again. This led to things getting even weirder though, since use of generic types in unit initialization clauses or class constructors was seemingly the straw that broke the camel’s back! However, I finally hit upon a solution, which was to enable compiler optimisations (the default is ‘off’ in the stock debug configurations). To do this, I went to Project|Options, chose the second node down (Delphi Compiler -> Compiling), ensured ‘Debug configuration – OS X platform’ was selected in the combo box at the top, then ticked the ‘Optimization’ box and finally clicked OK.

Notwithstanding this issue, overall I’d say remote debugging in XE2 and XE3 from a Windows guest to an OS X host is pretty smooth (if a bit bumpy once exceptions get raised), so don’t let this put you off…


Making the never-ending sorry tale of FMX menus a bit happier

$
0
0

For me, FMX at present has three main showstopping limitations: an inability to draw text that is neither wonky (XP/Vista) nor overly ‘sharp’ (W7/W8) on Windows; a TMemo that isn’t fit for purpose on any platform; and a menu implementation that is poor on Windows, and hopelessly broken on OS X. Well, perhaps not quite ‘hopelessly’ – while the only proper solution would be to untangle the source and use native menus throughout, in the meantime, I have found some workarounds for specific issues: in particular, three concerning TMainMenu (= the FMX interface for the native menu bar) and two concerning TPopupMenu.

Problem: the Mac menu bar is owned by the application, but FMX thinks it should be owned by a form

In a traditional Mac application whose raison d’être is to view or edit individual files, it is possible for the application to be running yet with no windows open, in which case you just have the application’s menu bar showing when you Cmd+Tab to it. Alas, but TMainMenu cannot fathom such a thing, since it thinks it must always be attached in spirit to a specific form even if it is not attached visually, as it would be on Windows. Put a TMainMenu on a data module, then, and nothing happens.

To get around this, define an interposer class for TMainMenu that firstly overrides RecreateOSMenu to do nothing, and secondly provides an explicit Activate method:

type
  TMainMenu = class(FMX.Menus.TMainMenu, IItemsContainer)
  protected
    procedure RecreateOSMenu; override;
  public
    procedure Activate;
  end;

//...

procedure TMainMenu.Activate;
begin
  IFMXMenuService(TPlatformServices.Current.GetPlatformService(
    IFMXMenuService)).CreateOSMenu(Application.MainForm, Self);
end;

procedure TMainMenu.RecreateOSMenu;
begin
  { do nothing - Activate method now controls this }
end;

You still need to provide a target form for Windows, but the CreateOSMenu code on OS X just ignores it (at least for now!). Anyhow, with the Activate method defined, you can now put a TMainMenu on an auto-created data module and (if you want that) only have the menu appear when running on OS X (since a menu bar will be there anyway).

Problem: when a menu item’s state is changed (e.g., its Enabled or Visible property is toggled), the FMX main menu code rebuilds the whole menu

This is just a complete WTF, but anyhow, sanity can be found in providing an alternative implementation of the IFMXMenuService interface, in harness with an interposer class for TMenuItem. For the details, see here.

Problem: if an item on a main menu has been assigned an action that controls its core properties (Enabled, Visible, Text, etc.) and the action doesn’t get updated independently, the action’s OnUpdate handler will never get called

To see this problem in action, do this:

1. Create a new FMX HD application, add a TEdit and TButton to the form, followed by a TActionList and a TMainMenu.

2. Add an action called actCopy to the action list, set its Text property to ‘Copy’, and handle its OnUpdate and OnExecute functions like this:

procedure TForm1.actCopyExecute(Sender: TObject);
begin
  (Focused.GetObject as TEdit).CopyToClipboard;
end;

procedure TForm1.actCopyUpdate(Sender: TObject);
var
  Obj: TFmxObject;
begin
  if Focused = nil then
    actCopy.Enabled := False
  else
  begin
    Obj := Focused.GetObject;
    actCopy.Enabled := (Obj is TCustomEdit) and
      (TCustomEdit(Obj).SelLength > 0);
  end;
end;

3. Add an top level item to the main menu, and a second level item under it which then gets assigned actCopy to its Action property. If you are using the TMainMenu interposer class discussed previously, add a call to MainMenu1.Activate in a handler for the form’s OnCreate event.

4. Run the application; notice that when the menu is pulled down, the Copy item is enabled regardless of whether the TEdit has the focus and has text selected.

Now, because I’m only interested in TMainMenu on OS X, the fix I’m about to suggest is Mac-specific. The principle should be similar on Windows though. Secondly, this fix presupposes the previous one, i.e. the alternative IFMXMenuService implementation and TMenuItem interposer class that doesn’t rebuild everything from scratch just because a single Enabled property has changed. If you cause the main menu to be completely recreated when you are handling one of its events, then surprise surprise, the OS doesn’t like it, and will in fact terminate the application there and then in the OS X case.

Anyhow, those caveats aside, take a copy of FMX.Platform.Mac.pas, add the copy to the project, and make the following edits to it:

  • Find the NewNSMenu method declaration, and change the parameter from Text: string to Source: TMenuItem.
  • Amend all calls to NewNSMenu accordingly (i.e., pass in the menu item itself, not the menu item’s text). There’s one call you won’t be able to adjust for, so comment it out and use this code instead:

//LNewMenu := NewNsMenu(FMXAppName);
LNewMenu := TNSMenu.Wrap(TNSMenu.Alloc.initWithTitle(NSSTR(FMXAppName)));
  • Where FMXOSMenuItem and TFMXOSMenuItem are defined, add the following interface and class on similar lines:

  FMXOSMenu = interface(NSMenu)
  ['{E0653081-A641-4220-B7D3-2F1CF87E071E}']
    procedure menuNeedsUpdate(menu: Pointer); cdecl;
  end;

  TFMXOSMenu = class(TOCLocal, NSMenuDelegate)
  strict private
    FSource: TMenuItem;
  public
    constructor Create(const ASource: TMenuItem);
    destructor Destroy; override;
    function GetObjectiveCClass: PTypeInfo; override;
    procedure menuNeedsUpdate(menu: Pointer); cdecl;
  end;

constructor TFMXOSMenu.Create(const ASource: TMenuItem);
begin
  inherited Create;
  FSource := ASource;
  UpdateObjectID(NSMenu(Super).initWithTitle(NSSTR(DelAmp(ASource.Text))));
  NSMenu(Super).setDelegate(Self);
end;

destructor TFMXOSMenu.Destroy;
begin
  NSMenuItem(Super).Release;
  inherited;
end;

function TFMXOSMenu.GetObjectiveCClass: PTypeInfo;
begin
  Result := TypeInfo(FMXOSMenu);
end;

procedure TFMXOSMenu.menuNeedsUpdate(menu: Pointer); cdecl;
var
  I: Integer;
  Item: TMenuItem;
begin
  try
    for I := FSource.ItemsCount - 1 downto 0 do
    begin
      Item := FSource.Items[I];
      if Item.Action <> nil then
        Item.Action.Update;
    end;
  except //an unhandled exception here will kill the program
    Application.HandleException(ExceptObject);
  end;
end;
  • Amend the implementation of NewNSMenu itself to look like this:

function TPlatformCocoa.NewNSMenu(const Source: TMenuItem): NSMenu;
var
  AutoReleasePool: NSAutoreleasePool;
begin
  AutoReleasePool := TNSAutoreleasePool.Create;
  try
    Result := NSMenu(TFMXOSMenu.Create(Source).Super);
    FNSHandles.Add(AllocHandle(Result));
  finally
    AutoReleasePool.release;
  end;
end;
  • After ensuring OS X is the target platform, re-run the application: the ‘Copy’ menu item should now disable itself as appropriate.

The background to this code is that every top level menu item will be a NSMenu, and likewise every originating item in a multi-level menu setup, e.g. the ‘Recent’ item under ‘File’ that pops out a list of the recently-opened files. Any NSMenu can then be assigned a delegate object to handle certain events, one of which – menuNeedsUpdate – is called to allow you to update sub-items’ state as necessary. In our case, we have the delegate be NSMenu (or rather, a subclass of NSMenu) itself. In this capacity, the class then handles the menuNeedsUpdate message by asking sub-items’ actions to update themselves.

Now, there is a slight flaw in this fix, and in particular the TFMXOSMenu class, since in short, its destructor will never get called. This is an occupational hazzard of the fact that when you define an Objective-C class (as we do here) using the Delphi to Objective-C bridge, the reference counting of the core Objective-C/Cocoa part of instantiated objects and the reference counting of the Delphi part are completely separate from one another. Alas, but the TFMXOSMenuItem class of the original unit suffers from this same flaw. However, since the menu bar isn’t something you should be recreating at will – rather, it should be created at start up and left there until the application terminates – this isn’t a big deal in practice given OS X, like Windows, will reclaim all allocated resources when the process terminates anyhow.

Problem: TPopupMenu in FMX has no OnPopup event

With the VCL TPopupMenu, I frequently make use of its OnPopup event. Unfortunately no such event exists in the FMX equivalent. Nonetheless, an interposer class can fix that easily enough:

type
  TPopupMenu = class(FMX.Menus.TPopupMenu)
  strict private
    FOnPopup: TProc<TObject>; //or TNotifyEvent
  public
    procedure Popup(X: Single; Y: Single); override;
    property OnPopup: TProc<TObject> read FOnPopup write FOnPopup;
  end;

procedure TPopupMenu.Popup(X, Y: Single);
begin
  if Assigned(FOnPopup) then
    FOnPopup(Self);
  inherited;
end;

When the interposer class is in scope, you can now assign an OnPopup handler at runtime.

Problem: TPopupMenu forgets the order of hidden items

Say you have a popup menu whose items you conditionally hide (i.e., set their Visible properties to False) at various times. In such a situation, after every time the menu is shown, it will have pushed the hidden items to the top of the list, so that when you unhide them later, they will appear in a different position to what their were originally.

The underlying cause is that when a TPopupMenu is shown, it reassigns visible items’ Parent property to the control that will actually be ‘poping up’, before reassinging the properties back to itself once the popup closes. My preferred workaround is once more an interposer class, adding code to explicitly maintain the items’ order. Continuing with the previous example, the overridden Popup method should be rewritten to look something like this:

//TPopupMenu declaration as before...

type
  THiddenRec = record
    Item: TControl;
    Index: Integer;
  end;

procedure TPopupMenu.Popup(X, Y: Single);
var
  HiddenList: TList<THiddenRec>;
  Rec: THiddenRec;
  I: Integer;
begin
  if Assigned(FOnPopup) then
    FOnPopup(Self);
  //fix stock code not maintaining order of hidden items
  HiddenList := nil;
  try
    for I := ItemsCount - 1 downto 0 do
    begin
      Rec.Item := Items[I];
      if not Rec.Item.Visible then
      begin
        Rec.Index := Rec.Item.Index;
        if HiddenList = nil then
          HiddenList := TList<THiddenRec>.Create;
        HiddenList.Add(Rec);
        Rec.Item.Parent := nil;
      end;
    end;
    inherited;
    if HiddenList <> nil then
      for I := HiddenList.Count - 1 downto 0 do
      begin
        Rec := HiddenList[I];
        InsertObject(Rec.Index, Rec.Item);
      end;
  finally
    HiddenList.Free;
  end;
end;

That’s it for now. If nothing else, my patience is getting rather frayed…


FMX tip: buttons with images that still have nicely-aligned captions

$
0
0

Unlike its VCL equivalent, the FMX TSpeedButton has no Glyph property; similarly, TButton doesn’t have the image properties added to the VCL version in D2009, and in FMX, there’s no TBitBtn class to boot. The reason for this is that in principle, there is no need for class-specific image properties:

  1. Add a TImage to the design surface.
  2. Using the structure view (top left of the IDE), reparent the image to the desired button by dragging and dropping with the mouse. (It doesn’t matter whether the button is a TButton, which takes the keyboard focus, or a TSpeedButton which doesn’t.)
  3. Select the image and use the object inspector to set its HitTest property to False. This prevents it from receiving mouse clicks.

This approach works fine if the button doesn’t have a caption, but when it does, an issue arises concerning alignment: the text will remain centred in the button regardless of whether the image is aligned to a certain side, so that if the image is relatively large, text will appear partly on it, and partly on the normal background:

Poorly aligned button captions

If the button images are either left-aligned (as in this example) or right-aligned, you could manually fix up the alignment by prepending or appending spaces to the button’s Text property as appropriate, however this won’t work for top- or bottom-aligned images, and moreover, causes hassles if the button’s text comes from an assigned action. So, what would make for a more principled fix?

Well, internally, a button’s text is part of its style, which like the style for any other control is composed of more primitive FMX controls. The text, then, comes from a TText control, which is placed inside a layout control of some sort that also contains a TSubImage or TRectangle that defines the button background and border.

Given that, one way to fix the text alignment issue might be to reparent our TImage to the parent of the TText at runtime. If that sounds a bit hacky, that’s because it is, and worse, the changes made to control style handling in XE3 blogged about by Eugene Kryukov (EMBT) here make it precarious to boot. Happily however, there’s a better way: when the button’s style is loaded (or reloaded), adjust the internal TText’s Padding property so that it no longer overlaps the image.

To do this generically for a number of buttons on a form, select them all in the designer (they can be a mixture of TButton and TSpeedButton controls if you want), click to the Events tab of the Object Inspector, type ButtonApplyStyleLookup next to OnApplyStyleLookup, and press Enter. This will create a shared event handler for the buttons’ OnApplyStyleLookup event. In the code editor, add the following for its implementation:

procedure TForm1.ButtonApplyStyleLookup(Sender: TObject);
var
  Button: TCustomButton;
  Control: TControl;
  TextObj: TFmxObject;
begin
  Button := (Sender as TCustomButton);
  for Control in Button.Controls do
    if Control is TImage then
    begin
      TextObj := Button.FindStyleResource('text');
      if TextObj is TText then
        case Control.Align of
          TAlignLayout.alLeft:
            TText(TextObj).Padding.Left := Control.Width;
          TAlignLayout.alTop:
            TText(TextObj).Padding.Top := Control.Height;
          TAlignLayout.alRight:
            TText(TextObj).Padding.Right := Control.Width;
          TAlignLayout.alBottom:
            TText(TextObj).Padding.Bottom := Control.Height;
        end;
      Break;
    end;
end;

What we do here is cycle through the button’s controls looking for the TImage; when we find it, we then look for the TText style resource and adjust its Padding accordingly. Here’s the result:

Nicely aligned button captions

PS – if having read this far you’d still prefer a button class with a dedicated Bitmap property, check out Mike Sutton’s custom TSpeedButton descendant here.


FMX tip: ‘labels’ with selectable text

$
0
0

Say you are designing a form in FMX, and wish to use a read-only edit box. No problem: stick a TEdit on the form and set its ReadOnly property to False. However, doing this doesn’t give any visual indication the edit box’s contents can’t be changed. Moreover, what if you’re using a TEdit rather than a TLabel simply to allow users to copy the text, if they really want to, in which case the edit box should completely blend into the form background and not even have a border? A common example of this is the version number text shown in an application’s ‘about’ box, for example the Delphi IDE’s or Firefox’s.

In the VCL removing a TEdit’s background is no problem – set its ParentColor property to True, and its BorderStyle property to bsNone. Neither the Color, ParentColor not BorderStyle properties exist on the FMX TEdit however, since all these attributes are controlled by its style.

So, one’s first thought might be to create a custom style for the edit box. To do that, right click on the TEdit in the form designer, then choose Edit Custom Style… from the popup menu. Once the style editor is showing, head to the structure view, open up the control hierarchy, select the item labelled ‘background’, then click the little ‘delete’ button (top left). Choose ‘Apply and Close’, and both the edit box’s white background and its thin border should be gone. Great!

Custom TEdit style with default main style active

Alas, but there’s a problem – if you change the global style, the edit box’s custom style won’t be updated accordingly. E.g., here’s what happens when the ‘dark’ style is loaded:

Custom TEdit style with different main style active

The problem here is that custom styles set up at design time involve merely ‘copy and paste “inheritance”‘ from whatever style is used by the IDE at the time. Change the main style for an application later, and any custom style won’t have its underpinings updated accordingly.

While this inherent limitation of the FMX styling system is bad enough in the present scenario, it is even worse for things like customising a list box item’s style, since the default platform styles differ (as you might expect) on details like highlight colours – just try running XE3′s CustomListBox demo on OS X to see this in action. Another pain point is the XE3 platform styles relying on their own bitmaps, one per platform – since no care was taken for each control to refer to the same part of the main bitmap when running on Windows and OS X, customising a control’s style at design time can lead to half of it disappearing when the program is run on a Mac!

What we need to do, then, is not use a custom style, but correct the default style for a read-only edit box at runtime, specifically by manipulating things in a handler for the control’s OnApplyStyleLookup event. Learning what to modify exactly means delving into the source and the stock style files, preferably more than one in the case of the latter. To take the TEdit case, for example, while all the standard styles define its background by a control with a style element name of ‘background’, the control type depends upon the style. In the case of the ‘platform’ styles it is a TSubImage, which references part of one big platform-specific bitmap. In the case of the additional, vector-only styles however (like ‘dark’), a TRectangle is used. Given that, removing the background must be done in a way that is neutral between both primitive control types (i.e., TSubImage and TRectange). In fact, it’s a good idea to make the code as generic as possible, since it’s perfectly possible Embarcadero will change the internals once more in XE4 or whatever.

Anyhow, to cut to the chase, this is what I’ve come up with -

procedure TForm2.LabelEditApplyStyleLookup(Sender: TObject);
var
  Obj: TFmxObject;
begin
  Obj := (Sender as TCustomEdit).FindStyleResource('background');
  if Obj is TControl then TControl(Obj).Opacity := 0;
end;

Why make the background control transparent rather than just delete it? Ultimately, because I’m not keen on just randomly deleting internal objects. Further, talk of automatic reference counting (ARC) being introduced (see here) makes me nervous simple Free calls on FMX objects (*) will even be possible in the future given the inherent conflict between reference counting and the TComponent ownership pattern that FMX currently employs.

(*) And no, I’m not predicting ARC will be forced (or even introduced at all) for Win32/Win64 projects. I’m purely talking about FMX, on mobile and possibly desktop too given much of the framework will presumably cross compile between mobile and desktop platforms.

Fixing a TForm.BorderIcons bug in FMX/OS X

$
0
0

If you try removing biMaximize from a FireMonkey form’s BorderIcons property, you’ll find the ‘maximise’ (zoom) button on OS X (i.e., the green ‘traffic light’) resolutely staying both visible and enabled. The reason for the bug is the following code in FMX.Platform.Mac.pas, and it exists in both XE2 and XE3:

        if TBorderIcon.biMaximize in AForm.BorderIcons then
          Style := Style or NSWindowZoomButton;              

The problem here is that NSWindowZoomButton is not a valid member of a Cocoa window style ‘set’, a fact the compiler wasn’t able to pick up given Objective-C does not have strongly typed sets like Pascal. Further, you cannot in fact hide or disable the zoom button via the window style – instead, you get a reference to the actual button and hide or disable it ‘directly’.

As Apple’s UI guidelines say to disable an unwanted zoom button rather than hide it, that’s what we’ll do for our fix. So, having taken a copy of FMX.Platform.Mac.pas (or if you’ve been following this blog before, with your latest version of it open), head to TCocoaPlatform.CreateWindow. Next, comment out the lines quoted above (i.e. ‘if TBorderIcon…’). Then, immediately after the NSWin.initWithContentRect call, add the following:

    if (Style and not NSResizableWindowMask <> 0) and
      not (TBorderIcon.biMaximize in AForm.BorderIcons) then
    begin
      NSWin.standardWindowButton(NSWindowZoomButton).setEnabled(False);
      NSWin.standardWindowButton(NSWindowZoomButton).setTarget(nil);
    end;

Setting the ‘target’ to nil is necessary, otherwise the Enabled property will get automatically re-enabled – roughly, you can think of the button as having had an action assigned to it when created, and setting the Target to nil as like setting the Action property to nil again.


FireMonkey’s BoundsRect bug, and working around it

$
0
0

Just a quick one, but the BoundsRect property getter in the FireMonkey TControl is incorrectly implemented, returning (0, 0, Width, Height) rather than (Position.X, Position.Y, Position.X + Width, Position.Y + Height). At first you might be tempted to say the property just has different semantics to the VCL version, however the implementation of the property setter gives the lie to that. Steps:

  1. Create a new FireMonkey HD application.
  2. Add a button to the form.
  3. Handle the button’s OnClick event like this:

procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.BoundsRect := Button1.BoundsRect;
end;

Expected: nothing to happen. Actual: the button moves to the top-left corner of the form.

To work around this bug, either don’t read BoundsRect in the first place (it is after all just a small utility property), or put the following class helper in scope:

type
  TControlHelper = class helper for TControl
  strict private
    function GetBoundsRect: TRectF;
    procedure SetBoundsRect(const Value: TRectF);
  public
    property BoundsRect: TRectF read GetBoundsRect
      write SetBoundsRect;
  end;

//...

function TControlHelper.GetBoundsRect: TRectF;
begin
  Result.Left := Position.X;
  Result.Top := Position.Y;
  Result.Right := Result.Left + Width;
  Result.Bottom := Result.Top + Height;
end;

procedure TControlHelper.SetBoundsRect(const Value: TRectF);
begin
  SetBounds(Value.Left, Value.Top, Value.Width, Value.Height);
end;


Fixing a FireMonkey TCheckBox/TAction bug

$
0
0

Try this:

  • Create a new FireMonkey HD application, before adding a check box and an action list to the form.
  • Double click the action list and add an action to it; set the action’s AutoCheck property to True.
  • Assign the action just created to the check box’s Action property.
  • Double click the form to create a handler for its OnCreate event. In the handler, set the action’s DisableIfNoHandler property to False:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Action1.DisableIfNoHandler := False;
end;
  • Run the application; once showing, click the check box. Expected: its checked state is toggled. Actual: nothing happens
  • In fact, toggling the check box does work, however you need to double click the control for this to work. This is patently a bug caused by the IsChecked property setter getting called twice in the course of a single mouse click

To fix the problem, you can at a pinch create a suitable interposer class. Doing that won’t be very ‘clean’ however because the IsChecked property setter isn’t virtual, so do this instead:

  • Take a copy of FMX.Controls.pas, add it to your project, and open it up.
  • Find your way to the implementation of TCheckBox.MouseUp.
  • Ignoring the reams of ‘Live’Bindings support code, add a check for IsCheckedStored in front of IsChecked being toggled:

if IsCheckedStored then IsChecked := not IsChecked;

Rebuild the application, and the bug should be fixed.

How to survive without image lists in a FireMonkey project

$
0
0

In the VCL, toolbar icons (glyphs) are set up using the TImageList component. In FireMonkey however, TImageList does not exist, and even TToolBar itself is half the component it is in the VCL – rather than manage its own buttons, it’s just a simple container control like TPanel. To add buttons to a FMX TToolBar, you therefore drop TSpeedButtons onto it; and to add glyphs to the buttons, you add a TImage control to each one. On the face of it this leads to a simpler situation that had with the VCL; on the other though, it causes maintenance hassles in non-trivial applications if the same glyph is supposed to be used by different buttons, or if you come to want to change the icon set to a different one, given each image must be changed separately.

Being bothered about this issue, I’ve found relief is at hand from the FireMonkey styling system though. Let’s see it in action:

  • Create a new FireMonkey HD application.
  • Drop a TSpeedButton onto the form, and clear its Text property. Next, add a TPanel (sic.) to the button. Since TSpeedButton won’t let you do this directly, reparent the panel by dragging its node onto the button’s node in the Structure pane (top left).
  • Set the panel’s HitTest property to False, and its Align property to alClient. If you want to be able to drag the button around at designtime, set either the panel’s Padding property to (2, 2, 2, 2) or the button’s Margins property to (2, 2, 2, 2). Note the meaning of Padding and Margins is (weirdly) flipped round compared to the VCL – this is something I still haven’t got used to myself!
  • Add a TLayout to the form, and set its Visible property to False.
  • Add a TImage to the layout, load a suitable ‘Open File’ glyph into its Bitmap property, and set its StyleName to ‘FileOpenGlyph’. For ease of identification in the designer, set its Name property to ‘FileOpenGlyph’ too.
  • Set the panel’s StyleLookup property to ‘FileNewGlyph’. Note you’ll have to type it, as the name won’t be in the drop-down list.

At the end of all that, you should have something like this:

GlyphStyleDemo

How this works is that TPanel, as a styled control (TStyledControl descendant), gets its visual appearance from another control, or more exactly, a copy of another control – the ‘style’. This other control can be anything – all that is crucial is that its StyleName property is set to a value matched by the value of the StyleLookup property of the intended style subject(s).

Normally, custom styles are set up via a TStyleBook component. Using a style book means using the IDE’s FireMonkey style designer though, which is something only a programming masochist can enjoy. What isn’t sp obvious is that you don’t actually need to use TStyleBook in the first place however – any control that has its StyleName property assigned will be available as a custom style. So, doing just that is what we did in the demo.

Now, we placed the source TImage on a hidden TLayout given we didn’t want it to be shown at runtime – setting its own Visible property to False wasn’t an alternative, since that would have caused the control it styled to be invisible at runtime too! However, it may be the case that you would prefer not to clutter a form with the source glyphs at all. In a VCL scenario, this would mean putting the application’s image lists onto a data module, but given TImage instances are controls, we can’t do that here. Instead, we need to put the glyph images onto a dummy form, auto-created but not ever visible. In my own case I had a further requirement: the ability to switch at runtime to a different glyph set. This was necessary because I’m looking to support both dark and light UI styles, and currently working with monochrome icons. While that may change later, there was no harm in ensuring everything would work if it didn’t.

So, continuing with the demo, add another form to the project, name it ‘StdGlyphsForm’, and save its unit as App.Glyphs.Std.pas. Then, go back to the first form, cut the TImage to the clipboard, then paste it onto StdGlyphsForm. Because we do not want the new form to become the application’s ‘main’ form in the VCL/FMX sense, do not have it auto-created in the normal fashion (Project|Options, Forms). Instead, it needs to be created explicitly when the application starts up, and directly (i.e., using TGlyphsForm.Create not Application.CreateForm).

Before we get to that though, let’s set up an alternative icon set to test with. To do that, save everything, then duplicate App.Glyphs.Std.pas via File|Save As…; save it as App.Glyphs.Alt.pas, then rename the form to AltGlyphsForm. After that, change the contents of the image, then add the original version of the unit (App.Glyphs.Std.pas) back to the project as well. In principle, we can now choose which icon set to use at runtime by instantiating the appropriate form class (StdGlyphsForm or AltGlyphsForm); if we wanted to then switch sets at runtime, we can just free the first glyphs form, instantiate the second, and ask all ‘normal’ forms to update their styling.

When targting OS X pursuing such a plan works fine, but when targeting Windows there’s a problem: specifically, at one point in FMX.Platform.Win.pas, Screen.Forms[0] is used in lieu of Application.MainForm when the latter cannot be guaranteed to be initialised yet. Because of this, the first form that gets created – regardless of how – becomes the ‘owner window’ for subsequent forms at the Windows API level. If the first form created were to be one of our glyph container forms, bad things will then result.

One way to get around this is to ensure the real ‘main form’ creates the glyph container. However, this assumes you have a ‘main’ form in the first place, which when targeting OS X especially isn’t necessarily the case. After fiddling about the problem, my preferred solution at present is to create the glyph container, move its contents to something else that isn’t a form too (just a bare TComponent instance will do), then immediately free it. So, let’s do that with the demo, wrapping the necessary code in a simple singleton type.

For this, add a new unit to the project, and call it App.Glyphs.pas. Then, copy the following code into its interface section:

uses
  System.Classes;

type
  TGlyphsStyle = (gsStandard, gsAlternative);

  GlyphsManager = record
  strict private class var
    FGlyphsOwner: TComponent;
    FGlyphsStyle: TGlyphsStyle;
    class constructor Initialize;
    class destructor Finalize;
    class procedure SetGlyphsStyle(Value: TGlyphsStyle); static;
  public
    class property GlyphsStyle: TGlyphsStyle read FGlyphsStyle write SetGlyphsStyle;
  end;

Next, make its implementation section look like this:

uses
  FMX.Types, FMX.Forms, FMX.Styles, App.Glyphs.Std, App.Glyphs.Alt;

class constructor GlyphsManager.Initialize;
begin
  FGlyphsOwner := TComponent.Create(nil);
  FGlyphsStyle := gsAlternative;
  SetGlyphsStyle(gsStandard);
end;

class destructor GlyphsManager.Finalize;
begin
  FGlyphsOwner.Free;
end;

class procedure GlyphsManager.SetGlyphsStyle(Value: TGlyphsStyle);
var
  Comp: TComponent;
  I: Integer;
  SourceClass: TComponentClass;
  Source: TComponent;
begin
  if Value = FGlyphsStyle then Exit;
  case Value of
    gsStandard: SourceClass := TStdGlyphsForm;
    gsAlternative: SourceClass := TAltGlyphsForm;
  else
    Assert(False);
    SourceClass := nil; //avoid compiler warning
  end;
  FGlyphsOwner.DestroyComponents;
  Source := SourceClass.Create(nil);
  try
    for I := Source.ComponentCount - 1 downto 0 do
    begin
      Comp := Source.Components[I];
      FGlyphsOwner.InsertComponent(Comp);
      if Comp is TFmxObject then
      begin
        TFmxObject(Comp).Parent := nil;
        if Comp is TControl then
          TControl(Comp).SetNewScene(nil); //see below
      end;
    end;
  finally
    Source.Free;
  end;
  FGlyphsStyle := Value;
  { next lines only necessary because we aren't updating
    the style as such as well }
  for I := Screen.FormCount - 1 downto 0 do
    Screen.Forms[I].UpdateStyle;
end;

The code here should be pretty straightforward – when the glyph set is to be changed, the old glyphs are first destroyed, the new ones loaded, and the application’s forms asked to refresh their and their controls’ styling. The only slightly tricky thing is how we change an existing component’s owner – since the Owner property is read-only, we need to call InsertComponent on the new owner instead, the implementation of which first deassigns any existing owner. Further, since a FireMonkey control, like a VCL one, has both a parent and an owner, and both will destroy the control when the parent or owner is itself destroyed, we need to clear the parent too. Lastly, due to an oversight in the FireMonkey source, a further reference – to the ‘scene’ – needs to be manually cleared as well. If you check out FMX.Forms.pas, you’ll find TCustomForm calls SetNewScene(Self) in DoAddObject, but not SetNewScene(nil) in DoRemoveObject. The resulting dangling reference then causes access violations if not cleared.

To put the new unit in action, head back to the main form of the demo (i.e., the one with the button on it), remove the now empty TLayout, and add a couple of radio boxes called rdoStandard and rdoAlternative. Set the first’s IsChecked property to True in the Object Inspector, then handle its OnChange event as thus:

procedure TForm1.rdoStandardChange(Sender: TObject);
begin
  if rdoStandard.IsChecked then
    GlyphsManager.GlyphsStyle := gsStandard;
end;

Similarly, handle rdoAlternative’s OnChange event like this:

procedure TForm1.rdoStandardChange(Sender: TObject);
begin
  if rdoAlternative.IsChecked then
    GlyphsManager.GlyphsStyle := gsAlternative;
end;

Finally, add App.Glyphs to the form’s implementation section uses clause (Alt+F11) and run the application. If all goes well, clicking on the radio boxes should toggle the button glyph:

GlyphStyleDemo - standard
GlyphStyleDemo - alternative

One thing to watch out for is that if you save everything, close and then reopen the project and have the form open in the IDE without either StdGlyphsForm or AltGlyphsForm too, the button’s styling will revert to a normal TPanel. This is nothing to worry about – close the form, open one of the glyph forms, then reopen the form, and the image will be back.

At the end of the day, I’m pretty happy with all this. While there was the odd irritating oversight in the FireMonkey source to work around, overall I find the style system here producing a more convenient approach that the VCL – in particular, it’s much nicer to be able to refer to a glyph by name rather than a meaningless numerical index. Moreover, the cosmetic issue of the custom style apparently being ‘lost’ when the main form is open without one of the glyph forms alongside is just that – cosmetic. In contrast, if the form were a VCL one with a TToolBar referencing a TImageList on a data module, the IDE would silently clear the actual reference!

Postscript – using TSubImage

As a final note, there is one variant of my approach that you might wish to consider. This is to replace the individual TImage instances with individual TSubImage ones, which then reference a single big TImage (TSubImage isn’t automatically registered, annoyingly enough, so you’ll need to do what Jeremy North did for TStyleTag here first). The reason is that when a control is used as a style, a copy of it is taken – and one aspect of the copy in a TImage case will be the contents of its Bitmap property.

If this were the VCL that wouldn’t be an issue due to the fact TBitmap there implements a reference counting system internally. In other words, when you do DestBitmap.Assign(SourceBitmap) in a VCL application, no ‘deep copy’ of the source bitmap is made, similar to how DelphiString2 := DelphiString1 doesn’t cause character data to be duplicated ‘there and then’ – rather, copying only happens when one of the two bitmaps or strings is modified later (the jargon for this is ‘copy on write semantics’). Unfortunately, the FMX TBitmap does not implement this behaviour though, and because of that, doing DestBitmap.Assign(SourceBitmap) causes an immediate ‘deep copy’ of the source, duplicating the bitmap bits in memory. For a small application with a small number of glyphs that won’t matter, and perhaps it won’t matter much either in a bigger program. As an application grows you might want to at least consider swapping out the individual TImage controls for TSubImage ones though. Thanks to the flexibility of the styling system, the task shouldn’t be too hard – all existing style associations can be kept intact.


Writing a simple FireMonkey TListLayout implementation

$
0
0

In FireMonkey, the usual way to group controls is to use a ‘layout’ of some sort. Conceptually, a layout is just a container control with no visual appearance of its own, at least by default. This contrasts to something like a panel or group box, which is also a container but one that the user sees as such.

A few layout classes are provided with Delphi. The simplest is TLayout, which does nothing more than implement the basic defintion of a layout control; beyond it stands TScrollBox, TScaleLayout, TGridLayout and TFlowLayout. You can read about them in the help, though in a nutshell, they do the following -

  • TScrollBox shows scroll bars as its contents extend beyond its own visible client area (the scroll bars are auto-hidden if not needed)
  • TScaleLayout resizes its controls as it itself is resized
  • TGridLayout positions and sizes its constituent controls in fixed-sized cells
  • TFlowLayout positions controls like the words in a paragraph, first left to right (or right to left via a property setting), then top to bottom. Controls are then moved accordingly when the layout is resized.

While it isn’t a layout control strictly speaking, an honorary mention also goes to the FireMonkey TListBox, whose fixed-width items can host child controls.

That said, I was recently wanting a layout object to organise controls top aligned from from top to bottom. Unfortunately, none of the standard layout classes met my requirements, which were as thus:

  1. Each control should fill the parent’s client width, unless the standard Paddings and Margins properties indicate otherwise.
  2. However, a control’s height should be specific to the control.
  3. The first control would be located at the top of the parent, the second immediately below the first (perhaps with a standard gap), the third immediately below the second and so on.
  4. It should be easy to change the order of controls.
  5. Ideally a vertical scroll bar should show if the controls cannot fit.

Requirement (1) ruled out TFlowLayout, number (2) ruled out TListBox and TGridLayout, none of the requirements made TScaleLayout relevant, and only (5) could be serviced by TScrollBox. Writing a custom layout class for the task proved pretty easy however:

  1. Create a new package project, and set its ‘description’ to something appropriate under Project|Options, Description (e.g., ‘List Layout Control’).
  2. Add a new unit to the package, and add System.SysUtils, System.Classes and FMX.Types to the unit’s interface section uses clause.
  3. Following TFlowLayout and its peers, declare a class descending from TControl. Publish the usual FMX control properties inherited from (but not published by) the base class.
  4. Following TFlowLayout again, add a new published property called VerticalSpacing, typed to Single, and add overrides for the DoAddObject, DoInsertObject and DoRealign protected methods.
  5. Declare the usual Register procedure needed for registering a custom component with the IDE.

Following this, the unit’s interface section should look like this. I’ve also added a ComponentPlatforms attribute to say the class supports Windows and OS X (‘any’ target would be more exact to be honest):

uses
  System.SysUtils, System.Classes, FMX.Types;

type
  [ComponentPlatforms(pidWin32 or pidWin64 or pidOSX32)]
  TListLayout = class(TControl)
  strict private
    FVerticalGap: Single;
    procedure SetVerticalGap(const Value: Single);
  protected
    procedure DoRealign; override;
    procedure DoAddObject(AObject: TFmxObject); override;
    procedure DoRemoveObject(AObject: TFmxObject); override;
  published
    property Align;
    property Anchors;
    property ClipChildren;
    property ClipParent;
    property Cursor;
    property DesignVisible;
    property DragMode;
    property EnableDragHighlight;
    property Enabled;
    property Locked;
    property Height;
    property HitTest;
    property Margins;
    property Opacity;
    property Padding;
    property PopupMenu;
    property Position;
    property RotationAngle;
    property RotationCenter;
    property Scale;
    property TouchTargetExpansion;
    property VerticalGap: Single read FVerticalGap write SetVerticalGap;
    property Visible;
    property Width;
    property OnApplyStyleLookup;
    property OnDragEnter;
    property OnDragLeave;
    property OnDragOver;
    property OnDragDrop;
    property OnDragEnd;
    property OnClick;
    property OnDblClick;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnPainting;
    property OnPaint;
    property OnResize;
  end;

procedure Register;

The implementation of Register is as you would expect if you’ve ever written a custom VCL control, namely a simple call to RegisterComponents. DoAddObject and DoInsertObject then just call the inherited implementation before requesting the control realigns its children. Lastly, the VerticalGap property setter assigns the backing field before requesting a realignment too:

procedure Register;
begin
  RegisterComponents('Samples', [TListLayout]);
end;

{ TListLayout }

procedure TListLayout.DoAddObject(AObject: TFmxObject);
begin
  inherited;
  Realign;
end;

procedure TListLayout.DoRemoveObject(AObject: TFmxObject);
begin
  inherited;
  Realign;
end;

procedure TListLayout.SetVerticalGap(const Value: Single);
begin
  if Value = FVerticalGap then Exit;
  FVerticalGap := Value;
  Realign;
end;

The final thing to implement is the DoRealign override. As a bit of an aside, DoRealign itself embodies the XE2 to XE3 FireMonkey transition (a lot of good work done, but a lot still to complete) in microcosm: in XE2, there was just Realign, which was a public, virtual method. As implemented in TControl, it did a whole load of checks to see whether controls should be realigned before finally doing the actual realigning. This was bad design, since if you wished to customise how realignment is performed in a descendant of TControl, you had to duplicate all those initial checks in your Realign override. In XE3, in contrast, Realign has been devirtualised and instead paired with a virtual, protected DoRealign method. In the new scheme, Realign still performs all the initial checks it did before, however it then delegates to DoRealign to do the actual repositioning and resizing. All well and good, but the refactoring wasn’t quite finished – to prevent the possibility of recursive calls to Realign/DoRealign, DoRealign still needs to set a protected FDisableRealign field to True, then reset it to False once it has finished. Really Realign should do that for you though, wrapping the FDisableAlign assignments in a try/finally block – if that were done, FDisableAlign could then be withdrawn into strict private scope. Nonetheless, it’s not a big issue.

Anyhow, here’s what my DoRealign implementation looks like:

procedure TListLayout.DoRealign;
var
  Control: TControl;
  NextY, StdWidth: Single;
begin
  if ControlsCount = 0 then Exit;
  FDisableAlign := True;
  try
    NextY := 0;
    StdWidth := Width - Margins.Left - Margins.Right;
    for Control in Controls do
      if Control.Visible then
      begin
        NextY := NextY + Control.Padding.Top;
        Control.SetBounds(Margins.Left + Control.Padding.Left,
          NextY, StdWidth - Control.Padding.Right,
          Control.Height);
        NextY := NextY + Control.Height +
          Control.Padding.Bottom + VerticalGap;
      end;
  finally
    FDisableAlign := False;
  end;
end;

If you’re following along, save everything, switch to the Release build configuration before adding and compiling for the Win64 and OS X target platforms (if you only have the Starter edition, that’s fine, however there won’t be any platforms beyond Win32 to compile for). The first time you compile the package there will be a prompt for adding fmx to the package’s requires clause – accept it. Then, toggle back to the default Win32 target, right click on the BPL’s node in the Project Manager, and choose Install. Finally, for each target platform, add the DCU output folder to the IDE’s search path (Tools|Options, Environment Options -> Delphi Options -> Library, Library Path); if desired, also add the .pas folder to the IDE’s browse path. E.g., if I were to save custom control units under E:\Delphi\Lib, this would give a default DCU output folder for 32 bit Windows of E:\Delphi\Lib\Win32\Release. If you toggle the project’s build configuration for Debug and recompile, you can also add the ..\Debug folders to the debug DCU search path as well. [In case it weren't obvious, these instructions are in case you aren't familiar with how to manually install a custom control - FMX or VCL - in the IDE. If you are, then there's nothing particular to my example control, or shouldn't be.] If all goes well, TListLayout should now be available in the Tool Palette when designing a form.

Now, the DoRealign implementation explicitly fulfils requirements (1) to (3) in my original list. Requirement (4) is also implicitly met, since our DoRealign lays out controls in the order they appear in the Controls array property, and that order can be changed by setting a sub-control’s Index as desired. So, if MyPanel is at the bottom of the layout control, setting its Index to 0 will move it to the top. Requirement (5) can then be met simply by nesting the TListLayout inside a TScrollBox, and setting its Align property to alTop:

TListLayout demo

Here, the form has a top-aligned TToolbar (StyleLookup set to ‘HeaderItemStyle’, as the default toolbar style looks pretty ugly IMO!), followed by a client-aligned TScrollbox with its Padding set to (2, 2, 2, 2). This then contains a top-aligned TListLayout with VerticalSpacing set to 4, with the layout itself containing three top-aligned panels, each of which has its Margins set to (8, 8, 8, 8), a left-aligned TLabel added and a client-aligned TEdit too.


XE3 and Subversion/Google Code

$
0
0

Just a small heads up, but it has come to my attention that the XE3 IDE’s Subversion (SVN) integration does not support the older, pre-v1.7 Subversion format, at least out of the box. While there are interop issues with recent versions of TortoiseSVN (the Subversion client I actually use) too, the XE3 IDE goes a step further by just hanging when the ‘Open From Version Control’ command is used against a repository in the older format.

While this in practice won’t bother many people, Google Code (a free-as-in-beer way to publish open source code) still uses Subversion v1.6… which means anything Delphi-related there whose author chose Subversion precisely because the Delphi IDE provides SVN support in the box may now look slightly foolish… Anyhow, this is all just a slightly roundabout way of saying I’ve added a ZIP of the sample code for my XE2 book here!

[Edit - my initial post was overly negative about TortoiseSVN (thanks to Robert Love and M J Marshall for correcting me in the comments). The issue is that TortoiseSVN v1.7x forces you to upgrade working copies from v1.6x to v1.7x, which means you can't use both it and the XE or XE2 IDE interchangeably, assuming you haven't modified the IDE's behaviour to use the newer SVN DLLs. However, if you only use TortoiseSVN as your client, it can happily work with v1.6x servers. This though gives even less reason for the XE3 IDE to just hang!]


Annoying FireMonkey buglet/oversight of the week

$
0
0

My personal FireMonkey buglet/oversight of the week is this: setting a TTextControl descendant’s Text property inside its constructor doesn’t do anything. For those who don’t know, TTextControl is the base class for things like TLabel, TListBoxItem and TExpander in FMX. Create custom descendants of these, then, and the following sort of code will not work:

type
  TMyExpander = class(TExpander)
  public
    constructor Create(const AOwner: TComponent;
      const AText: string); reintroduce;
  end;

constructor TMyExpander.Create(const AOwner: TComponent;
   const AText: string);
begin
  inherited Create(AOwner);
  Text := AText;
end;

The reason is quite simple: TTextControl sets a flag in Create that prevents changes to the Text property doing anything until that flag is reset in an override of AfterConstruction. In true ‘FM squared’ fashion, the code is nevertheless convoluted enough to give you RSI from having to press F7 so much in trying to track this down. Argh!!!!


Viewing all 62 articles
Browse latest View live