A FMX TClipboard and TMacPreferencesIniFile implementation

On first experimenting with FMX and OS X back last September/October, I wrote both a FMX TClipboard implementation and a TCustomIniFile descendant that delegates to the Mac Preferences API. The latter serves as a rough analogue of TRegistryIniFile on Windows; as for the former, I wrote it because the stock FMX clipboard support is crap. If anyone is interested, the source is available on Google Code here (the SVN URL is http://delphi-foundations.googlecode.com/svn/trunk/FMX%20Utilities/). Suffice to say, I don’t claim any particular originality (another FMX TClipboard implementation is floating about here for example), however both classes were written without reference to alternative implementations.

TMacPreferencesIniFile (CCR.MacPrefsIniFile.pas)

In terms of approach, my Mac Preferences wrapper uses the native typing as appropriate when writing, but string-based auto-conversions as required on read. To that extent, it behaves similarly to TRegistryIniFile.

In itself, the Mac Preferences API only directly supports a single level of settings – in other words, a preferences file (‘plist’) is just a flat list of keys and values. This is a problem for any TCustomIniFile descendant that wraps it, since the TCustomIniFile interface assumes at least two levels (‘sections’, then keys-and-values). In order to create a hierarchy, the Apple-promoted workaround is to write a CFDictionary (or NSDictionary, if using the Objective-C interface) for a ‘value’. However, this is a rather impractical solution due to dubious API decisions on Apple’s part – in order to change a single value more than one level down, a CFDictionary-based approach requires you to load the whole branch, copy it to a ‘mutable’ CFDictionary, make the amendment, then rewrite the whole branch again.

Instead of that, I have adopted the approach taken by both TextWrangler and Microsoft Word, which is to fake a preference hierarchy by using a delimiter character embedded in the ‘real’ key names. By default a colon is used, like TextWrangler (Word uses a backslash), though you can change this by setting the Delimiter property. If you write a value for the ‘Width’ key under the ‘General’ section, then, what actually happens is that a plist key called ‘General:Width’ is set.

Other than that, all of the TCustomIniFile interface is supported, with native implementations of ReadBinaryStream and WriteBinaryStream too. Lastly, since the Preferences API requires writes to be explicitly flushed, so does my wrapper, in the form of a call to the UpdateFile method. This is just like TMemIniFile, which requires you call UpdateFile to affect any changes too.

For more info about TMacPreferencesIniFile, check out either the source and/or a small demo I’ve written for it:

TClipboard (CCR.FMXClipboard.pas)

My FMX TClipboard class has the aim of providing the capabilities of the VCL equivalent for both Windows and OS X, no more no less: for example, it has an AsText property and HasFormat method, can be assigned to and from an FMX TBitmap, has basic custom clipboard format support, and supports adding multiple formats concurrently using the Open and Close methods. Its core functionality is used like this:

uses CCR.FMXClipboard;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnIdle := ApplicationIdle;
end;

procedure TForm1.ApplicationIdle(Sender: TObject; var Done: Boolean);
begin
  btnPasteImage.Enabled := Clipboard.HasFormat(cfBitmap);
  btnPasteText.Enabled := Clipboard.HasFormat(cfText);
end;

procedure TForm1.btnCopyImageClick(Sender: TObject);
begin
  Clipboard.Assign(ImageControl1.Bitmap);
end;

procedure TForm1.btnCopyTextClick(Sender: TObject);
begin
  Clipboard.AsText := Memo1.Text;
end;

procedure TForm1.btnCopyImageAndTextClick(Sender: TObject);
begin
  Clipboard.Open;
  try
    Clipboard.AsText := Memo1.Text;
    Clipboard.Assign(ImageControl1.Bitmap);
  finally
    Clipboard.Close;
  end;
end;

procedure TForm1.btnPasteImageClick(Sender: TObject);
begin
  ImageControl1.Bitmap.Assign(Clipboard);
end;

procedure TForm1.btnPasteTextClick(Sender: TObject);
begin
  Memo1.Text := Clipboard.AsText;
end;

The cfText and cfBitmap thing imitates how the main FMX framework relabels the Windows ‘virtual key’ constants (i.e., VK_LEFT becomes vkLeft and so on). However, cfXXC values are typed (albeit weakly) to a TClipboardFormat opaque type. On Windows, instances of this type directly map to unsigned integer IDs like CF_TEXT and CF_BITMAP; on OS X, they are CFStringRef instances that hold OS X string IDs under the bonnet.

In order to register a custom format, you must call the global Clipboard object’s RegisterFormat method, passing a string identifier. Technically, OS X doesn’t require custom formats to be ‘registered’ at all. However, RegisterFormat provides consistency between platforms, returning a TClipboardFormat value you can use with HasFormat, various Assign overloads, and ToBytes and ToStream. The accompanying demo illutrates their use:

11 thoughts on “A FMX TClipboard and TMacPreferencesIniFile implementation

  1. Hi! Very nice implementations!
    Though I would have added another button to your demo of the clipboard, demonstrating how can the clipboard be used for copying different formats at a time, so targeting Notepad, Word and Paint at once.
    The button on click handler would be just as follows:

    procedure TfrmClipboardDemo.Button1Click(Sender: TObject);
    begin
      Clipboard.Open;
      try
        Clipboard.Assign(ImageControl1.Bitmap);
        Clipboard.AsText := Memo1.Text;
        //Any other format can also be added at the same time, before closing.
      finally
        Clipboard.Close;
      end;
    end;
    

    Again, very good job.
    Best regards,
    Ruben

    • Good idea! I’ve updated the published demo and the post itself, thanks (supporting multiple formats in one go is designed, as indicated by the Mac backend source in particular).

  2. Hi! After examining CCR.MacPrefsIniFile i му noticed that this unic an only be used in OSX-targeted app. So, maybe it will be better to implement cross-platform one? On Windows, this unit can delegate all calls to IniFile, but on OSX it will use plist.

    Reason: you can write cross-platform code without iFDEFs – for me it makes code look much more clear and elegant!)

    Next hint: publish this code on GoogleCode/GitHub separately, because of its very usefull stuff! IMHO, it can be included in RTL, but if not – make it more accessible than hiding it inside deeps of SVN for your demo-pack!

    • Hello

      Did you see the demo? The idea is that you code to TCustomIniFile (i.e., the abstract base class), with just one IFDEF when the ‘ini file’ object is created.

      That said, aside from your suggestion, another option is to have TMacPreferencesIniFile read and write using the XML plist format when targeting Windows. That would live up to the first part of the name ‘TMacPreferencesIniFile’ – in contrast, delegating to an internal TMemIniFile would live up to the second. I’m leaning towards a third (simpler) option however – adding a separate TCustomIniFile factory function to CCR.MacPrefsIniFile.pas:

      function CreatePlatformIniFile(
        const APreferredFileName: string = '': TCustomIniFile;
      {$IFDEF MACOS}
      begin
        Result := TMacPreferencesIniFile.Create;
      end;
      {$ELSE}
      begin
        if APreferredFileName <> '' then
          IniFileName := APreferredFileName
        else
        begin
          //!!!lookup company and product name from VERINFO resource?
        end;
        Result := TMemIniFile.Create(APreferredFileName);
      end;
      {$ENDIF}
      

      Thinking as I type, the factory function could be expanded into a small singleton –

      type
        TUserSettings = record
        strict private
          class var FCompanyName, FProductName: string;
          class constructor Initialize;
        public
          class function CreatePlatformIniFile: TCustomIniFile;
          class property CompanyName: string read FCompanyName write FCompanyName;
          class property ProductName: string read FProductName write FProductName;
        end;
      
      class constructor TUserSettings.Initialize;
      {$IFDEF MACOS}
      begin
        //... lookup values for FProductName etc. from 
        //the application bundle
      end;
      {$ELSE}
      begin
        //... lookup values for FProductName etc. from 
        //the version info resource, if it exists, otherwise
        //initialise FProductName to Application.Title
      end;
      {$ENDIF}
      
      end;
      

      Any thoughts, in particular with respect to identifying a suitable default location for the INI file under %APPDATA%?

      • You can have multiply ini files for your app. For ex, first file can be initialization for your installation of app (locale, license name/location, resource path etc), and this file should not be modified on user’s machine after installation. So it can be placed in Program Files.

        Next one can be actual settings to be readied and writed during life of app, and it should definitely go to AppData (and I hate to have any settings or auto-generated files in my document folder)).

        About code: I like your idea about factory class(function?), that can be customized for platform. This can remove IFDEF from app code and leaves them only in lib unit, and it’s ok) Code will be definitely cleanely!

        Maybe there can be some factories that can init instance with Company name? (some apple style like NewObjectWithCompanyName) And if some 3-d party libs are installed, use JCL to deal with VersionInfo?

        • You can have multiple ini files for your app.
          Indeed, in which case you may choose to avoid the Mac Preferences API completely and use TMemIniFile on OS X too. More exactly, the settings you suggest should live under Program Files would live in the application bundle on OS X, leaving the AppData settings to be stored using the Preferences API.

          About code: I like your idea about factory class(function?), that can be customized for platform.
          Check out the revision in the SVN repository…

          And if some 3-d party libs are installed, use JCL to deal with VersionInfo?
          I’m not going to create a dependancy on a gigantic 3rd party library. I don’t even make my own FMX-related classes depend on each other…

  3. Pingback: My FMX TClipboard and TMacPreferencesIniFile implementations now compiling in XE4 | Delphi Haven

  4. Pingback: Free Clipboard Component And Sample Code For Delphi XE5 Firemonkey On Android And IOS | Delphi XE5 XE6 Firemonkey, Delphi Android, Delphi IOS

  5. Hi,
    I try to use your example, but i have error’s

    i just want to make a picture copy to clipboarb (Windows)
    can you help me ?

    my delphi version is XE3,

    if BitsSize 0 then
    Move(Bitmap.starline^, Ptr^, BitsSize);

    the compilator say : [dcc32 Erreur] CCR.BitmapCopyAndPaste.pas(236): E2003 Identificateur non déclaré : ‘starline’

    Sorry fo my english
    Thank you

Leave a comment