C++ Tutorials‎ > ‎

The Window Manager

In this part of the tutorial we'll use the menubar from the previous part to allow us to create new windows, drag them about the screen, and close them. Exciting stuff!

Program listing:

#include <MacWindows.h>
#include <Events.h>
#include <Dialogs.h>
#include <ToolUtils.h>
#include <Sound.h>

// constants
const short defaultMenubar = 128;
const short menuApple = 128;
const short menuFile = 129;
const short menuitemAbout = 1;
const short menuitemNew = 1;
const short menuitemQuit = 2;
const short defaultWindow = 128;

// globals
if __MWERKS__ == 0
QDGlobals qd;
#endif

// function prototypes
void Initialize();
void MainLoop();
void Terminate();
void doMenuBar(long menuAction);

void main() {
    Initialize();

    MainLoop();

    Terminate();
}

// Initialize
void Initialize() {
    InitGraf(&qd.thePort);
    InitFonts();
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs(nil);
    InitCursor();

    Handle menubar = GetNewMBar(defaultMenubar);
    SetMenuBar(menubar);
    DrawMenuBar();
}

void MainLoop() {

    EventRecord event;

    while (true) {
        if (WaitNextEvent(everyEvent, &event, 10L, nil)) {
            if (event.what == mouseDown) {
                WindowPtr clickedWindow;
                short clickedPart = FindWindow(event.where, &clickedWindow);
                if (clickedPart == inMenuBar) {
                    doMenuBar(MenuSelect(event.where));
                } else if (clickedPart == inDrag) {
                    DragWindow(clickedWindow, event.where, &qd.screenBits.bounds);
                } else if (clickedPart == inGoAway) {
                    if TrackGoAway(clickedWindow, event.where, clickedPart) {
                        DisposeWindow(clickedWindow);
                    }
                } else if (clickedPart == inContent) {
                    if (clickedWindow != FrontWindow()) {
                        SelectWindow(clickedWindow);
                    }
                }
            } else if (event.what == updateEvt) {
                BeginUpdate((WindowPtr) event.message);
                EndUpdate((WindowPtr) event.message);
            }
        }
    }
}

void Terminate() {
    ExitToShell();
}

void doMenuBar(long menuAction) {
    if (menuAction <= 0) {
        return;
    }

    short menu = HiWord(menuAction);
    short item = LoWord(menuAction);
    if (menu == menuApple) {
        if (item == menuitemAbout) {
            SysBeep(1);
        }
    } else if (menu == menuFile) {
        if (item == menuitemNew) {
            GetNewWindow(defaultWindow, nil, (WindowPtr) -1);
        } else if (item == menuitemQuit) {
            Terminate();
        }
    }
    HiliteMenu(0);
}

Line-by-line description:

const short menuitemNew = 1;
const short menuitemQuit = 2;

Note that we made a minor change to the constants. We added menuitemNew and moved menuitemQuit down to position 2.

void doMenuBar(long menuAction);

We also added a new function that handles the menubar decision tree. This makes the main loop a lot smaller and easier to read. Speaking of which, our main loop has been almost entirely rewritten. Let's take a look at it.

while (true) {
    if (WaitNextEvent(everyEvent, &event, 10L, nil)) {

We changed GetNextEvent() to WaitNextEvent() so that our program will play nicer with others. This didn't matter much when our program was just a menubar that only allowed you to beep or quit, but our new program has windows that can be dragged around the screen, and they now have to deal with windows from other applications (being placed over them or under them, for example.) The 10L parameter tells the Event Manager that if we don't have any events pending, we will be willing to wait 10 ticks (about 1/6th of a second) for other programs to do processing. This is called cooperative multitasking and it is a pervasive feature of Classic Mac OS. You should read more about it if on the internet if you want to understand how it works.

if (event.what == mouseDown) {
    WindowPtr clickedWindow;
    short clickedPart = FindWindow(event.where, &clickedWindow);
    if (clickedPart == inMenuBar) {
        doMenuBar(MenuSelect(event.where));
    } else if (clickedPart == inDrag) {
        DragWindow(clickedWindow, event.where, &qd.screenBits.bounds);
    } else if (clickedPart == inGoAway) {
        if TrackGoAway(clickedWindow, event.where, clickedPart) {
            DisposeWindow(clickedWindow);
        }
    } else if (clickedPart == inContent) {
        if (clickedWindow != FrontWindow()) {
            SelectWindow(clickedWindow);
        }
    }

Our mouseDown decision tree has been greatly expanded. We handle three new cases now: inDrag, inGoAway, and inContent. 

inDrag is the case where the user clicks or drags on the title bar of a window. The Window Manager provides a function that makes window dragging very easy, and it is conveniently named DragWindow. The third parameter of the function specifies the screen region that dragging is limited to. &qd.screenBits.bounds specifies the entire screen.

inGoAway is the case where the user clicks the close button on a window. A function called TrackGoAway() waits until the user releases the mouse button, and returns true if they released it while staying within the close button, and false if they moved it out. This lets the user change their mind halfway through a potentially disastrous click that could close an important unsaved document. If they really did mean to close the window, we call DisposeWindow() to close it.

Finally, inContent is the case where the user clicks any other part of the window. In our demo program, the only thing we will do is check to see if the window is the current active window, and bring it to the front if it is not.

} else if (event.what == updateEvt) {
    BeginUpdate((WindowPtr) event.message);
    EndUpdate((WindowPtr) event.message);
}

This bit of code is more for the sake of other programs right now than for our own. One bizzare thing about Mac OS is that because of the cooperative multitasking, you often have to do things for other programs. When a window has a region that needs to be redrawn (because another window was dragged over it, for instance), the Event Manager will send out an updateEvt. Calling BeginUpdate() tells the Event Manager to give that window some time to update itself. If our windows had anything inside them, we would call their draw function in between BeginUpdate() and EndUpdate(). We don't, but we still have to call these functions for the sake of other programs that might be running. If you want to see what would happen without them, try commenting out these lines and running your program. Create a window and drag it across the window containing your code file. You should see the places where your window was dragged leave behind a blank white trail. That's because the other program's window didn't get notified about the updateEvt by you.

The last piece of code is the doMenuBar function. It is the same as the code that was previously in the event loop with one change:

if (item == menuitemNew) {
    GetNewWindow(defaultWindow, nil, (WindowPtr) -1);

If the user clicks the "New" menu item, we create a new window with resource ID defaultWindow. There are some important things to note here that are not immediately obvious. First is that GetNewWindow() returns a WindowPtr to the newly created window. We don't need it in our program, so we just ignore it, but usually you should save it in a variable and do something with it. 

The next thing is the second parameter to GetNewWindow(). This is a WindowPtr that provides GetNewWindow with a place to store the window data. If it is passed nil instead, then the new window is stored on the heap. If you manage your window's data yourself, you must free the memory when the window is closed by first calling DisposePtr() and then CloseWindow() on the window pointer. If you allow the Window Manager to manage your memory for you, you only have to call DisposeWindow() and you don't have to keep track of pointers yourself, but it is possible for memory fragmentation to occur on the heap more easily. In a program where windows can be created and destroyed frequently, you will want to manage window memory yourself.

Finally, the last parameter is a WindowPtr to the window that the new window should be placed in front of. Casting -1 to a WindowPtr tells the Window Manager to put the new window on top of all the other windows, in the front.

We are almost ready to run the program, but first we need to add some new resources. Open your .rsrc file and create a new WIND resource. The default settings are fine, save and close the WIND menu. Now go to your MENU resources and edit the File menu. Add a new menu item and name it "New." Move it so that it's above the "Quit" option by dragging it. Save your .rsrc file and quit ResEdit.

Compile and run your program. When you click File->New, a window should be created. This window can be dragged around the screen and closed by clicking the close button. You can create as many windows as you want. (Until you run out of memory, anyways.)





Comments