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);
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!
Done 🙂
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.
Hi Chris,
Thanks for the post – I have used your NSWindowOfForm function to show a Webview (Webkit) window. For some reason all I get is a white background.
I have uploaded the project to http://almayga.com.au/WebKit.zip
Any chance you can have a quick look at it?
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.
Hi Chris,
thanks for the fullscreen tip!
is there a possibility to get notified when user clicked on fullscreen arrows in window title bar?
I see a delegate for this: http://developer.apple.com/library/mac/#documentation/General/Conceptual/MOSXAppProgrammingGuide/FullScreenApp/FullScreenApp.html
But i cannot get it to work in Delphi. I think of events OnEnterFullscreen OnLeaveFullscreen. Any suggestion?
Thanks and best regards
Dirk
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!
Thanks Chris, I’m looking forward for your demo, I tried it, but wasn’t so easy.
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.
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.
Great, thanks!
full screen mode works perfectly!!
thanks+++