« How to Make a Full Screen App

Posted by Brian Christensen on July 15, 2001 [Feedback (11) & TrackBack (0)]

// Introduction

There are a number of reasons why you may want your app to "take over" the screen. If you're writing a game, for instance, hiding the menubar, dock, desktop and other applications will make your game more immersive. You can design your own UI elements to create a customized look & feel without clashing with the rest of the system, as everything else will be hidden and unavailable while you're game is running. The same concept applies to many multimedia applications as well. It's also worth noting that many users have probably come to expect games and multimedia applications to take control of the screen. Obviously it requires more effort to create custom interface graphics and UI elements, however users will appreciate and enjoy using your game or multimedia application more if you put this extra effort into it. Make sure you don't go overboard either though -- creating an entire operating system along with every type of UI element you could ever dream of inside of your game is unnecessary and only adds extra complications and bulk. Keep it as simple as possible. This tutorial will guide you through the steps necessary to take over the screen. Additionally, it will show you how to put up a window with some content to play around with.

The result
Figure 1: Look ma, no menubar!

 

// The Basics

If you haven't done so already, launch Project Builder and create a new project (if you're lost at this point, you might want to read some of the articles in the Bare Basics section). Do all the usual stuff (create a Controller object, wire it up as a "delegate" of the file owner, etc).

The Controller
Figure 2: Wiring up the controller.

Since we're going to be using the CoreGraphics framework, you'll need to add that to the project. CoreGraphics is part of the ApplicationServices framework, so add "/System/Library/Frameworks/ApplicationServices.framework" via the "Add Frameworks..." command in the "Project" menu.

 

// The Code

Let's get down to business! Before we implement the actual methods, we need to add this to our "Controller.h" file (additions are shown in bold):

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

@interface Controller : NSObject
{
    NSWindow *mainWindow;
}
@end

Now that we have the basic bookkeeping things out of the way, we can continue on to actually taking over the display (commonly referred to as "acquiring" the display as well).

 

// Acquiring the Display

Since our Controller is a delegate of NSApplication, all we need to do is implement the applicationDidFinishLaunching: method:

- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
    int windowLevel;
    NSRect screenRect;

We use the CGDisplayCapture() function to put up a so-called "shielding window." This will prevent other applications from displaying annoying dialogs and doing other things that may interfere with the immersiveness of your app or game. It should be noted at this point that there is another function called CGCaptureAllDisplays(), which will put up a shielding window on all monitors connected to the computer. Many games do this, but it's arguable whether or not it's a good idea (ie. some users might want to keep Mail open in the secondary monitor, to see when new e-mails arrive while playing your game).

    // Capture the main display
    if (CGDisplayCapture( kCGDirectMainDisplay ) != kCGErrorSuccess) {
        NSLog( @"Couldn't capture the main display!" );
        // Note: you'll probably want to display a proper error dialog here
    }

CGDirectDisplay puts the shielding window on a window level above everything else on the screen. This means that if we try to put up a new window now, it will appear behind the shielding window and we won't be able to see it. So what we need to do is use the CGShieldingWindowLevel() function to get the window level of the shielding window. This will allow us to position our content window in front of the shielding window.

    // Get the shielding window level
    windowLevel = CGShieldingWindowLevel();

We use the NSScreen class' frame method to get the size of the main screen.

    // Get the screen rect of our main display
    screenRect = [[NSScreen mainScreen] frame];

Here we create the window using the NSBorderlessWindowMask styleMask (which basically omits the title bar).

    // Put up a new window
    mainWindow = [[NSWindow alloc] initWithContentRect:screenRect
                                styleMask:NSBorderlessWindowMask
                                backing:NSBackingStoreBuffered
                                defer:NO screen:[NSScreen mainScreen]];

Now comes the interesting part: setting the window level. You might be asking yourself now why we even bother using CGDirectDisplay to capture the display if we can simply set the window size to the size of the screen and then set the window level to something above the menubar and dock layers. Well, the shielding window really makes sure it's above anything else and returns a safe window level for us to use. Additionally, the shielding window has the nice side effect of preventing the user from command-tabbing to another application.

    [mainWindow setLevel:windowLevel];

I imagine the rest is fairly obvious:

    [mainWindow setBackgroundColor:[NSColor blackColor]];
    [mainWindow makeKeyAndOrderFront:nil];
}

 

// Relinquishing the Display

In order to kill the shielding window when it's time to quit, add the applicationWillTerminate: method to your Controller.m file. Keep in mind that if you used CGCaptureAllDisplays() above, you'll need to use CGReleaseAllDisplays() here instead of CGDisplayRelease(). Theoretically you can use CGReleaseAllDisplays() even if you previously only captured one (the main) display, however in my (unscientific) testing CGReleaseAllDisplays() appeared to be a bit slower than calling CGDisplayRelease().

    - (void)applicationWillTerminate:(NSNotification *)notification
    {
        [mainWindow orderOut:self];
        
        // Release the display(s)
        if (CGDisplayRelease( kCGDirectMainDisplay ) != kCGErrorSuccess) {
        	NSLog( @"Couldn't release the display(s)!" );
        	// Note: if you display an error dialog here, make sure you set
        	// its window level to the same one as the shield window level,
        	// or the user won't see anything.
        }
    }

You're probably thinking "great, so I have an empty full screen window, what now?" Keep reading!

 

// Adding a Content View

Let's add a content view! Switch to Interface Builder and add a "Panel" to your MainMenu.nib file (you'll find that in the same place as the windows and drawers). Wire it up to an outlet (ie. "slideShowPanel") in the Controller object. Don't forget to add the IBOutlet id slideShowPanel instance variable to your Controller.h file.

The Panel
Figure 3: Let's add some content.

Resize it to 640x480. This is the minimum size you can expect people to be running your application at. If we use the autosizing attributes in Interface Builder, we can have all of the objects resize and reposition themselves based on the actual screen size. To accomplish this, click on the "My Slide Show" text item and select "Size" from the inspector window. Specify the following autosizing options:

Autosizing Options
Figure 4: The autosizing options.

What does this mean? The text box keeps its size, but is repositioned to maintain the same relative distance from all edges of the window. For more autosizing examples, choose "Help" in Interface Builder and type in "autosizing." As an exercise, try adding a button and have it stay at the bottom right of the screen.

 

// More Code

Add this code to the end of your applicationDidFinishLaunching: method. What it does is resize the panel (thus invoking the use of our autosizing attributes) and loads it as the content view of our window:

    // Load our content view
    [slideShowPanel setFrame:screenRect display:YES];
    [mainWindow setContentView:[slideShowPanel contentView]];

Notice that you'll be able to use this same technique to switch between different content panels. Well, that's pretty much it! There are some things you should keep in mind though, so keep reading.

 

// Keyboard Events

Our window won't get any keyboard events the way it is, due to the fact that it was created with with the NSBorderlessWindowMask style mask. Apparently windows of this type can't become "key windows". However, you can fix this problem by subclassing NSWindow and overriding the canBecomeKeyWindow method.

    - (BOOL)canBecomeKeyWindow
    {
        return YES;
    }

This tidbit is thanks to Aaron Hillegass of Big Nerd Ranch.

 

// Resolution Switching

I won't cover resolution switching in this article, but it is an alternative to dealing with the autosizing stuff. However, autosizing allows you to do some pretty clever things. For example, in a slide show app you could load different images based on the size of the screen. Experiment a bit and figure out what's best for your particular app.

If you do want to implement resolution switching, I recommend you take a look at the OmniGroup sample code. In addition to rez switching, this highly useful code demonstrates a couple of other game related things, such as cursor manipulation and creating an OpenGL context.

 

// Additional Resources

If you're planning on writing a game or multimedia-type application, you might want to check out some of these mailing lists and links:

  • mac-games-dev Mailing List
    "This mailing list is sponsored by Apple Computer and is intended to be a discussion forum for the people who write, code, design, and otherwise create games for the Mac OS and Mac OS X." It should be added that a lot of the discussions on this list are related to Carbon development, but you can still learn a lot by subscribing.
  • x-game-dev Mailing List
    "A forum for exchange of ideas/solutions for game development on Mac OS X using Cocoa/Obj-C/OpenGL/OpenAL etc. This forum should be a bit more specific and hands-on, so we would love to have people who are working on projects...:)"
  • iDevGames.com
    An excellent site for Mac game developers. Most of the articles there can applied to Cocoa game development as well as Carbon. Definitely worth checking out.
  •  

    // Conclusion

    Now that you know how to make a full screen app, I want to see some good multimedia software and games! If you have any questions or comments about this tutorial, feel free to contact me.


    Comments

    Very well written! Thanks alot. Now I just need to go see if it works ... =)

    Posted by: Alan on January 19, 2003 08:04 AM

    Well I can vouch that it does work, and very well at that. Thanks!

    Posted by: brian on March 1, 2003 11:07 PM


    Hi, very nice, thank you! How do you allow important system dialogues to appear above the shielding window? I'm thinking specifically of the "low battery level" dialogue; more than one popular game doesn't let you see the warning before your computer shuts itself off!

    Thanks!

    Posted by: strauss on March 6, 2003 10:58 AM

    i can't bring my own panels forward after I do this. Is there a way to let my own input panels come forward, or am I stuck with ONLY one window that's fullscreen from now on?

    Posted by: guy on May 26, 2003 11:55 PM

    When I compile it Project Builder complains about the method declaration is not in the class context, can anyone help me?

    Posted by: Alex on August 6, 2003 01:48 AM

    Alex, be sure that you've placed your method declaration between the @implimentation and @end markers.

    Posted by: Holmes on August 8, 2003 01:02 AM

    I subclassed the canBecomeKeyWindow method to return YES, and it still doesn't call keyDown when a key is pressed. At this point I also subclassed the acceptsFirstResponder to see if that would work, but it didn't. What needs to be done in order to respond to keyDown events?

    Posted by: stef on August 28, 2003 02:11 PM

    Instead of using

    CGShieldingWindowLevel();

    you should use

    NSMainMenuWindowLevel + 1

    - this allows you to still open popup menus.

    Posted by: Max on September 20, 2003 05:34 AM

    Very nice tutorial. A couple questions though:

    How do I get a custom view to display in full screen mode?

    Also, is there any way to make the menu bar visible in full screen mode?

    Thanks.

    Posted by: Integer on October 5, 2003 02:39 AM

    For my purposes I've had success using BeginFullScreen() in Quicktime.

    Posted by: Feanor on October 6, 2003 11:19 PM

    Stef:

    I couldn't get the keys to come through at first either after subclassing the window class. Turns out you have to call makeKeyAndOrderFront on the window AFTER you set the content view and I was doing it before. Worked fine after that.

    Posted by: Ron on November 14, 2003 10:48 AM
    Post a comment