views:

45

answers:

2

I am developing a C++ app and I need to display a NSWindow with a WebKit WebView inside it. I've coded up the Objective-C class which will manage creating and displaying the window but the WebView contained inside it does not display. Here is my code. Any idea on what is wrong and how I can fix it?

I'm compiling the below code with $g++ -x objective-c++ -framework Cocoa -framework WebKit Foo.m main.m -o test

Foo.h

#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>

@interface Foo :NSObject {
 NSWindow *window;
 WebView *view;
}

- (void)displayWindow;

@end

Foo.m

#import "Foo.h"

@implementation Foo

- (id)init {

 self = [super init];

 // Window Container
 window = [[NSWindow alloc] initWithContentRect:NSMakeRect(500.0f,500.0f,250.0f,250.0f)
           styleMask:NSBorderlessWindowMask
             backing:NSBackingStoreNonretained
            defer:NO];

 // WebView
 view = [[WebView alloc] initWithFrame:NSMakeRect(0, 0, 250.0f, 250.0f)
        frameName:@"Frame"
        groupName:nil];

 [[view mainFrame] loadHTMLString:@"<html><head></head><body><h1>Hello</h1></body></html>" 
        baseURL:nil];

 return self;
}

- (void)displayWindow {
 NSLog(@"In Display window");

 [window setContentView:view];
 [window setLevel:NSStatusWindowLevel];
 [window orderFrontRegardless];

 sleep(5); // leave it up for 5 seconds

}

- (void)dealloc {
 [window release];
 [super dealloc];
}

@end

main.m

#import "Foo.h"

int main() {

 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
 [NSApplication sharedApplication];

 Foo *foo = [[Foo alloc] init];
 [foo displayWindow];
 [foo release];

 [pool release];

 return 0;
}
+3  A: 

You need to run the run loop. If you just order the window in and then exit, that's exactly what will happen: The window will appear, and then (five seconds later) your program will exit. You can run the run loop by telling the application (which you create but don't otherwise use) to run.

On the main thread of a Cocoa app, sleep is always the wrong answer. The same goes for its Cocoa cousins, +[NSThread sleepUntilDate:] and +[NSThread sleepForTimeInterval:]. The run loop will let you tell it to run for a fixed amount of time, but that won't get the application running; you do need to send the application the run message, which provides no opportunity to exit after a fixed interval.

The solution there is to first create an NSTimer object whose target is the application and whose selector is @selector(terminate:). Create it scheduled and non-repeating, with the interval set to five seconds. (Creating it scheduled means you don't need to schedule it separately—it is already ready to go from the moment you create it.) Then, send the application the run message. Five seconds later, the run loop will fire the timer, which will tell the application to terminate itself. This is assuming that you actually have a good reason to make your application quit after five seconds.

As noted by Yuji, every window in modern Cocoa should use NSBackingStoreBuffered.

And don't forget to release what you have created; you currently are forgetting that in the case of the view. See the Memory Management Programming Guide for Cocoa.

Once you have this working, I suggest moving toward a more typical architecture for this application:

  • Create a subclass of NSObject, and make an instance of that class your application's delegate.
  • Put the window and its WebView into a nib, and have the app delegate create a window controller to load and own the contents of that nib.
  • The app delegate should also be responsible for loading the page into the WebView and for setting up the self-termination timer.
  • Finally, create a nib to hold your application's main menu (the contents of the menu bar) and the application delegate. Interface Builder has a template for the first part; you create the app delegate object by dragging a blank Object in from the Library, setting its class on the ⌘6 Inspector, and dragging the connection from the application to the object. Then, you can reduce main to the single line that Xcode's project templates put in it: return NSApplicationMain(argc, argv);.

Doing all this will help your understanding of Cocoa, as well as your maintenance of the application—cramming everything into main will not scale.

You should also read the Cocoa Fundamentals Guide, if you haven't already.

Peter Hosey
Could you add to your excellent answer my comments on the backing store? I scratched my head for an hour why his code doesn't work when I did obvious things ...
Yuji
Yuji: Good catch. Done.
Peter Hosey
+1  A: 

Don't make it sleep. It stops the execution of the main thread, in which the GUI is dealt with. Instead, you need to run the run loop. Also, Cocoa needs to set itself up. So, call [[NSApplication sharedApplication] run] to set it up correctly and run the event loop.

Also, don't use backing mode other than buffered mode. Other modes are remnants from the time immemorial, and only NSBackingStoreBuffered should be used. As discussed in this Apple document, the non-retained mode is a remnant to support Classic Blue Box (OS 9 virtualizer), and newer classes like WebKit just can't operate within it.

So, what you need to do is practically:

  1. change NSBackingStoreNonretained to NSBackingStoreBuffered.
  2. Remove the line

    sleep(5);
    
  3. add a line

    [[NSApplication sharedApplication] run];
    

    after

    [foo displayWindow];
    
  4. Also, in order for an app to receive events from the window server correctly, you need to pack it into an app bundle. Compile it into a binary called foo, and create the following structure:

    foo.app/
    foo.app/Contents/
    foo.app/Contents/MacOS/
    foo.app/Contents/MacOS/foo   <--- this is the executable
    

    Then you can double-click foo.app from the Finder, or just call ./foo from the command line.

Yuji