NSWindow tips and tricks

When targeting OS X, a FireMonkey form is a native Cocoa window, albeit with a custom-drawn and custom-managed client area. While FMX exposes some functionality of the underlying Objective-C class (NSWindow), there’s still quite a bit it doesn’t. Nonetheless, NSWindow methods can be called easily enough via XE2’s Delphi to Objective-C bridge. This post provides a few examples.

Getting the NSWindow of a FMX form

The first problem is how to convert a TForm reference to a NSWindow one in the first place (in Delphi, NSWindow is an interface type declared in Macapi.AppKit). As a point of principle, FMX is almost the complete opposite of the VCL when it comes to exposing native API hooks. I think this a mistake, but regardless, the NSWindow case is fortunately an exception to the general rule. Here’s my preferred way:

uses
  Macapi.ObjectiveC, Macapi.CocoaTypes, Macapi.Foundation, Macapi.AppKit,
  FMX.Platform, FMX.Platform.Mac;

type
  TOCLocalAccess = class(TOCLocal);

function NSWindowOfForm(Form: TCommonCustomForm): NSWindow;
var
  Obj: TOCLocal;
begin
  Obj := (FmxHandleToObjC(Form.Handle) as TOCLocal);
  Result := NSWindow(TOCLocalAccess(Obj).Super);
end;

Constraining a form’s size

To prevent the user from making the form any bigger than a certain size, call either setContentMaxSize or setMaxSize; the former determines the maximum size of the client area, and the latter the maximum frame size. Normally you would want to use setContentMaxSize:

procedure SetFormMaxClientSize(AForm: TCommonCustomForm;
  AMaxClientWidth, AMaxClientHeight: Single);
var
  MaxClientSize: NSSize;
begin
  MaxClientSize.width := AMaxClientWidth;
  MaxClientSize.height := AMaxClientHeight;
  NSWindowOfForm(AForm).setContentMaxSize(MaxClientSize);
end;

procedure TForm1.UpdateMaxSizeToImage1;
begin
  SetFormMaxClientSize(Self, Image1.Position.X + Image1.Width,
    Image1.Position.Y + Image1.Height);
end;

Setting the maximum size of a window on OS X actually does two things – aside from preventing the user from resizing the form beyond a certain point, it also determines how big the form gets when the green traffic light (‘maximise button’ in Windows terms) is clicked. This allows you to achieve one of the key components of a Proper Mac App: driving Windows users insane by having the maximise button not actually maximise the window! More seriously, the Mac principle is for maximising a window to maximise to the document, not to the screen. Admittedly, I’m not quite sure how this explains iTunes’ behaviour, but hey, Apple can do what it likes with its own stuff I guess…

Setting a form’s minimum size

Doing the converse of setContentMaxSize and setMaxSize are setContentMinSize and setMinSize. Once again, you’ll probably want to set the minimum client area size rather than the minimum total window size:

procedure TForm1.FormCreate(Sender: TObject);
var
  MinClientSize: NSSize;
begin
  MinClientSize.width := 200;
  MinClientSize.height := 100;
  NSWindowOfForm(Self).setContentMinSize(MinClientSize);

Load and save the form’s position and size using the Mac preferences system

procedure TForm1.FormCreate(Sender: TObject);
begin
  NSWindowOfForm(Self).setFrameAutosaveName(NSSTR(ClassName));
end;

The setFrameAutosaveName method sets the preferences key name used and loads any previously-saved state. Because of that, it’s best to call it in an OnCreate handler. There is no need to do anything else – the state will be saved automatically when the form closes.

Allow dragging a form by clicking anywhere in the client area, not just the title bar.

NSWindowOfForm(Self).setMovableByWindowBackground(True);

If you enable this, be warned clicking a button will still click the button – it’s just the form will be dragged as well!

Associate a form with a file

This one adds a clickable document icon to the form’s title bar – right click it, and a popup menu for browsing the folder hierarchy is displayed:

procedure TForm1.btnOpenFileClick(Sender: TObject);
begin
  if not dlgOpen.Execute then Exit;
  Memo1.Lines.LoadFromFile(dlgOpen.FileName);
  NSWindowOfForm(Self).setRepresentedFilename(NSSTR(dlgOpen.FileName));
end;

Create a PDF of the client area of a form

procedure CreatePDFFromForm(AForm: TCommonCustomForm; ADestStream: TStream); overload;
var
  Data: NSData;
  Rect: NSRect;
begin
  Rect.origin.x := 0;
  Rect.origin.y := 0;
  Rect.size := NSSize(Platform.GetClientSize(AForm));
  Data := NSWindowOfForm(AForm).dataWithPDFInsideRect(Rect);
  if (Data = nil) or (Data.length = 0) then RaiseLastOSError;
  ADestStream.WriteBuffer(Data.bytes^, Data.length);
end;

procedure CreatePDFFromForm(AForm: TCommonCustomForm; const AFileName: string); overload;
var
  Stream: TFileStream;
begin
  Stream := TFileStream.Create(AFileName, fmCreate);
  try
    CreatePDFFromForm(AForm, Stream);
  finally
    Stream.Free;
  end;
end;

In use:

procedure TForm1.btnCreatePDFClick(Sender: TObject);
var
  OutputFileName: string;
begin
  //expand to (e.g.) /Users/chrisrolliston/Documents/Test.pdf -
  //OS X is a bit fussy about non-expanded file names
  OutputFileName := ExpandFileName('~/Documents/Test.pdf');
  //create the PDF!
  CreatePDFFromForm(Self, OutputFileName);
end;

This creates a PDF screenshot, in essence. Thus, if you have a memo on the form and half the text isn’t scrolled in view, then half of the text won’t be outputted to the PDF. Even still, creating a PDF screenshot is nice trick I think.

Enabling OS X Lion’s full screen mode

Soon after I originally posted, Iván Pons in the comments figured out how to add the ‘toggle full screen mode’ button to the title bar (it gets shown top right):

{$IF NOT DECLARED(NSWindowCollectionBehaviorFullScreenPrimary)}
const
  NSWindowCollectionBehaviorFullScreenPrimary = 128;
{$IFEND}

procedure TForm1.FormCreate(Sender: TObject);
begin
  NSWindowOfForm(Self).setCollectionBehavior(NSWindowCollectionBehaviorFullScreenPrimary);
end;

According to Apple guidelines, if full screen mode is available, you should add an ‘Enter Full Screen’ menu item to the View menu. To implement this you need to call the toggleFullScreen method of NSWindow, which isn’t declared on the stock NSWindow translation in XE2. No fear though! A combination of the dynamic nature of Objective-C and the flexibility of XE2’s Delphi to Objective-C bridge has this sorted:

type
  NSWindowLion = interface(NSWindow)
  ['{7AED4558-D490-4450-8CBA-4A6FC1796C25}'] //GUID can be anything unique
    procedure toggleFullScreen(sender: Pointer); cdecl;
  end;

  TNSWindowLion = class(TOCGenericImport<NSWindowClass, NSWindowLion>);

function NSWindowLionOfForm(Form: TCommonCustomForm): NSWindowLion;
begin
  Result := TNSWindowLion.Wrap((FmxHandleToObjC(Form.Handle) as ILocalObject).GetObjectID);
end;

procedure TForm1.itmToggleFullScreenClick(Sender: TObject);
begin
  NSWindowLionOfForm(Self).toggleFullScreen(nil);
end;

Note that you will need to have had previously enabled full screen mode with the setCollectionBehavior call before toggleFullScreen will actually do anything.

Postscript – printing

This one doesn’t work properly unfortunately – while the print dialog panel is shown correctly and output created, there’s some sort of resolution issue that messes up the result, at least for me:

NSWindowOfForm(Self).print(nil);
Advertisements

18 thoughts on “NSWindow tips and tricks

  1. Very interesting.

    One question, do you know how to activate full screen mode?

    I see that it’s using setCollectionBehavior and constant NSWindowCollectionBehaviorFullScreenPrimary (apple’s help), but I can’t manage to work.

    In fire monkey doesn’t exist NSWindowCollectionBehaviorFullScreenPrimary

    Any idea?

    • I wrote this post before both update 4 came out and upgrading my iMac to Lion. Checking out full screen mode is on my list of things to look at next!

  2. Done 🙂

    unit unidad.mac.os;
    
    interface
    
    uses
      Macapi.ObjectiveC, Macapi.CocoaTypes, Macapi.Foundation, Macapi.Appkit,
        FMX.Platform, FMX.Platform.Mac, FMX.Forms;
    
    procedure fullScreenWindow(Form: TCommonCustomForm);
    
    type
      TOCLocalAcces = class(TOCLocal);
    
    implementation
    
    uses
      fmx.dialogs, sysutils;
    
    procedure fullScreenWindow(Form: TCommonCustomForm);
    var
      Obj: TOCLocal;
      window: NSWindow;
    const
      NSWindowCollectionBehaviorFullsScreenPimary = 128;
    begin
      Obj:= (FMXHandleToObjC(Form.Handle) as TOCLocal);
      window:= NSWindow(TOCLocalAcces(Obj).Super);
      window.setCollectionBehavior(NSWindowCollectionBehaviorFullsScreenPimary);
    end;
    
    • That was quick, thanks! I’ve updated the post with your tip, and extended to show how to explicitly toggle full screen mode once it is enabled.

    • Sort answer: WebView is not just a Cocoa class, but a Cocoa control, and FMX doesn’t support hosting native controls at present. An FMX form is a native form under the bonnet, but a FMX control is a completely custom affair.

      • Thanks for the clarification Chris. My question then is where does this leave a Delphi developer looking to utilize some of these other Cocoa controls? Without Webkit for example it leaves us with no embeddable webbrowser or richedit control for OSX. Would it be possible to have theese controls/windows created in a dylib in Xcode and then called from a Delphi app? Any ideas or suggestions is much appreciated.

      • ‘where does this leave a Delphi developer looking to utilize some of these other Cocoa controls?’

        In an analogous position to the VCL developer looking to utilise .NET controls.

        ‘Would it be possible to have these controls/windows created in a dylib in Xcode and then called from a Delphi app?’

        It isn’t creating them that’s the problem (from what you say about the ‘white background’, you appear to have actually done that), it’s integrating them. Similar to how you can’t just drop a WinForms or WPF control onto a VCL form, despite everying boiling down to Windows API calls, so you can’t just drop a Cocoa control onto a FMX form, even though both share commonalities such as NSApplication, NSWindow, etc.

  3. Hello Christ
    Do you know a way to insert buttons or other controls into the form caption (toolbar) area of a osx window like standart osx applications.

    • Sorry, I’m not sure what you mean. Do you have an example of a pure Cocoa application that has extra buttons like that?

      PS – Christ? I don’t sound that high and mighty do I? 😉

      • Hello
        I just want to use the toolbar area of a OSX window. In IB it is combined with the titlebar so the form caption and the toolbar buttons are in a uniform panel. One approach may be to change the height of the title bar and put the controls in this area. But the thing that I couldnt manage is to get the the NSView reference of Firemonkey controls.
        And a final note: I have purchased your book, and yes you sound that mighty 🙂

      • Ah, the Cocoa toolbar widget. I’ll have a look and roll up a demo in the next few days. Basically, you call the NSToolbar API, and it adjusts the client area accordingly, a bit like adding a standard menu bar in a Windows form.

        Thanks for buying the book by the way!

      • The process is a variant of the NSAlert one I go through in the book. I’ve got the buttons themselves working, however I have an issue with the built-in customisation UI that needs fixing.

  4. Avid Delphi developer but left for other development pastures. Now find myself coming back to Delphi and need a quick brush up. I have purchased your book from Amazon and looking forward to reading it. Also book marked your site.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s