Small Nerd Example Chapter 2

Aaron Hillegass’ book, “Cocoa Programmig for Mac OS X” is a great way to learn Cocoa, Objective-C and Interface Builder, and I highly recommend you buy it to understand OS X programming (of course, you’ll be buying the new, improved Second Edition, and I’m working off the first edition here, so things may not completely match up). In order to demonstrate how compact, yet readable, Renaissance + Python can be, I’m going to convert the example programs from this book. Aaron’s company is called Big Nerd Ranch, and I was trying to find something to show which postings were examples from the book, so readers can follow along, but without implying in any way that Aaron has approved of these conversions in any way, so I’m going to call them the Small Nerd Examples (The small nerd being me).

Note that while you are developing, it is inconvenient to have to build the project every time you touch a file, so you can use python setup.py py2app –alias to build it once, then you can edit your files normally and the changes will be reflected when you run your application. Just remember to re-run this when you add a new file, and to re-run it without the alias flag when you’re building any version to be distributed.

Like the previous example, Hello World, this one contains four files. The setup.py file will look radically similar to the Hello World version. The program itself is simple: A window with two buttons, one which seeds the random number generator with the current time, and another which generates and displays a random number between 1 and 100 in a text field.

MainMenu.gsmarkup

<?xml version="1.0"?>
<!DOCTYPE gsmarkup>
<gsmarkup>
    <objects>
        <menu type="main">
            <menu title="ch02" type="apple">
                <menuItem title="About ch02" action="orderFrontStandardAboutPanel:"/>
                <menuSeparator/>
                <menu title="Services" type="services"/>
                <menuSeparator/>
                <menuItem title="Hide ch02" action="hide:" key="h"/>
                <menuItem title="Hide Others" action="hideOtherApplications:"/>
                <menuItem title="Show All" action="unhideAllApplications:"/>
                <menuSeparator/>
                <menuItem title="Quit ch02" action="terminate:" key="q"/>
            </menu>
            <menu title="Window" type="windows">
                <menuItem title="Minimize Window"
                    action="performMiniaturize:" key="m"/>
                <menuSeparator/>
                <menuItem title="Bring All to Front"
                    action="arrangeInFront:"/>
            </menu>
            </menu>
                <menu title="Help" type="help">
                <menuItem title="ch02 Help" keys="?"/>
        </menu>
    </objects>
</gsmarkup>

MainWindow.gsmarkup

<?xml version="1.0"?>
<!DOCTYPE gsmarkup>
<gsmarkup>
    <objects>
        <window delegate="#NSOwner" resizable="no">
            <vbox>
                <button target="#NSOwner" action="seed"
                    title="Seed random number generator with time"/>
                <button target="#NSOwner" action="generate"
                    title="Generate random number"/>
                <textField editable="no" id="text" align="center"/>
            </vbox>
        </window>
    </objects>
    <connectors>
        <outlet source="#NSOwner" target="text" key="outputNum"/>
    </connectors>
</gsmarkup>

ch02.py

'''
Hillegass Example, Ch. 02
'''
from Foundation import *
from AppKit import *
from Renaissance import *
import random
class AppDelegate(NSObject):
    outputNum = None

    def windowWillClose_(self, notification):
        NSApp().terminate_(self)

    def quit_(self, notification):
        NSApp().terminate_(self)

    def close_(self, notification):
        NSApp().terminate_(self)

    def applicationDidFinishLaunching_(self, notification):
        NSBundle.loadGSMarkupNamed_owner_('MainWindow', self)

    def seed(self):
        random.seed()

    def generate(self):
        self.outputNum.setStringValue_(str(random.randint(1,100)))

def main():
    app = NSApplication.sharedApplication()
    delegate = AppDelegate.alloc().init()
    app.setDelegate_(delegate)
    NSBundle.loadGSMarkupNamed_owner_('MainMenu', delegate)
    NSApp().run()
if __name__ == '__main__': main()

setup.py

'''
Run with:
% python setup.py py2app
or
% python setup.py py2app --alias # while developing
'''
from distutils.core import setup
import py2app
setup(
    data_files = ['MainMenu.gsmarkup', 'MainWindow.gsmarkup'],
    app = ['ch02.py'],
)

OK, this time out we’ve got about 52 lines of XML markup and 44 lines of Python. Of course, the program doesn’t really do much more than Hello World, but we’re getting somewhere. If anyone has questions about the code, or want more explanatory text around the Renaissance markup, please let me know in the comments or by email.

Hello Renaissance

In my last post I promised a Hello World program for PyObjC + Renaissance. If you haven’t got those installed, or aren’t sure, please check out the prerequisites.

We’ll be creating four files, each of which will be a template for upcoming examples. The menus will be defined in MainMenu.gsmarkup, the application window will be in MainWindow.gsmarkup, the application code will be in hello.py, and the py2app build script will be in setup.py. There is no reason that the menus and window have to be separated this way, but it will serve as an example for later, more complex applications, when you’ll want to load in code from multiple files.

MainMenu.gsmarkup

<?xml version="1.0"?>
<!DOCTYPE gsmarkup>
<gsmarkup>
    <objects>
        <menu type="main">
            <menu title="Hello World" type="apple">
                <menuItem title="About Hello World"
                action="orderFrontStandardAboutPanel:"/>
                <menuSeparator/>
                <menu title="Services" type="services"/>
                <menuSeparator/>
                <menuItem title="Hide Hello World" action="hide:" key="h"/>
                <menuItem title="Hide Others" action="hideOtherApplications:"/>
                <menuItem title="Show All" action="unhideAllApplications:"/>
                <menuSeparator/>
                <menuItem title="Quit Hello World" action="terminate:" key="q"/>
            </menu>
            <menu title="Edit">
                <menuItem title="Cut" action="cut:" key="x"/>
                <menuItem title="Copy" action="copy:" key="c"/>
                <menuItem title="Paste" action="paste:" key="v"/>
                <menuItem title="Delete" action="delete:"/>
                <menuItem title="Select All" action="selectAll:" key="a"/>
            </menu>
            <menu title="Window" type="windows">
                <menuItem title="Minimize Window" action="performMiniatureize:"
                key="m"/>
                <menuSeparator/>
                <menuItem title="Bring All to Front" action="arrangeInFront:"/>
            </menu>
        </menu>
    </objects>
</gsmarkup>

MainWindow.gsmarkup

<?xml version="1.0"?>
<!DOCTYPE gsmarkup>
<gsmarkup>
    <objects>
        <window title="Hello World" closable="NO" >
            <vbox>
                <label>Hello World!</label>
                <button title="Click this button to quit" action="terminate:"/>
            </vbox>
        </window>
    </objects>
</gsmarkup>

hello.py

from Foundation import *
from AppKit import *
from Renaissance import *
class MyApplicationDelegate(NSObject):

    def cut_(self, sender):
        pass

    def applicationDidFinishLaunching_(self, notification):
        NSBundle.loadGSMarkupNamed_owner_('MainWindow', self)

def main():
    app = NSApplication.sharedApplication()
    delegate = MyApplicationDelegate.alloc().init()
    app.setDelegate_(delegate)
    NSBundle.loadGSMarkupNamed_owner_('MainMenu', delegate)
    NSApp().run()
if __name__ == '__main__': main()

setup.py

'''
Minimal setup.py example, run with:
% python setup.py py2app
'''
from distutils.core import setup
import py2app
setup(
    data_files = ['MainMenu.gsmarkup', 'MainWindow.gsmarkup'],
    app = ['hello.py'],
)

Commentary

OK, so 80 lines of code may seem excessive for a Hello World program. We could certainly do it in less, perhaps 20 total lines of Python and Renaissance. But what we have here is a complete working Cocoa application which behaves properly to standard keyboard shortcuts, supports services, etc. And that’s not bad for 80 lines of code.

Losing my Nibs

I’ve been working with PyGame a lot and it’s been hard slogging. PyGame is great for moving images around quickly, but I’m more interested in vector drawing, native UI widgets, and higher-level events than I can get from PyGame.

So lately I’ve been turning my attention more and more toward PyObjC. I’m writing games for my kids, who are playing them on OS X, and I’m much less willing to make sacrifices to get cross-platform behaviour. And when I only have a few hours a week to write code, I need to focus on high productivity tools. So I’m writing Cocoa code, using Python, and it’s great. Bob Ippolito, Bill Bumgarner, Ronald Oussoren, Jack Jansen and the rest of the MacPython developers have produced an amazingly great tool. on top of Apple/Next’s highly evolved Cocoa framework. The mapping is so good that standard Cocoa references work for me to bootstrap my way up the Cocoa learning curve, my favorite reference being AppKiDo, which gives a series of views on every class (being able to separate instance methods of a class from all the intance methods it inherits, for example). Great stuff.

My only problem with Cocoa development (besides all the things I still have to learn about it, I’m not yet a proficient Cocoa developer) is the reliance on Interface Builder and NIB files. I think Interface Builder is one of the best visual layout tools I’ve used, and it’s great as far as it goes, but the problem is that you pretty much have to use it. I’ve tried to create applications which built their own interfaces from code, but I was unable to get the menus working because the Cocoa framework expects you to load the menus from a NIB at startup. I’m sure it can be done, but I couldn’t figure out how, even with the help of the MacPython mailing list and wiki.

And NIB files, which is how Interface Builder stores the interface, are a problem for me too, because they are serialized objects, not descriptions, so they are binary goop which is inaccessible except to Interface Builder. I can’t grep them or do a global search-and-replace if I change a method name. It’s hard enough for me to find the right places in IB to hook into my application, but finding all the places later which I need to change when my code changes rapidly becomes nightmarish.

And all this leads to examples like Build a text editor in 15 minutes or An example using NSDocument which consist of a few lines of code (the power of Cocoa) accompanied by tons of screenshots of Interface Builder and paragraphs of instructions for wiring up your application. Even worse, as the second example demonstrates, since the author no longer hosts that article and Google doesn’t cache the images, the screenshots no longer exist.

One more gripe. Because IB stores serialized objects, it doesn’t pick up changes to your code for those object unless you specifically force it to. So what I want in a UI tool:

  • Declarative
  • Simple and powerful
  • Able to replace NIB files and Interface Builder
  • Text-based, not binary
  • Agile for rapid development, adapts to rapidly changing code
  • Able to specify simple apps completely, in code, without resorting to pictures or talking the user through mouse gestures

And the best part is that I think I’ve found it. More in my next post.

« Previous Page « Previous Page Next entries »

google

google

asus