Python with a Cocoa GUI on macOS

I finally had a chance to try writing a native Cocoa interface in python. My test project was based on a post by Adam Wisneiwski (though I had to use the cached google version due to a bad gateway error). In that post, Adam laid out the process to put together a basic python app that uses a native cocoa GUI created in Xcode. Given the updates to Xcode, and the fact that I’m using anaconda python, I figured I’d repost my process, with the modifications I made to get it to work in case it is useful for anyone else (and so I can remember how I got it to work).

First, some background: I have anaconda python installed in my home directory, so I had to add some packages so python can connect with the Mac Cocoa framework. If you are just running vanilla python that comes with your Mac, then you shouldn’t need to add anything. Test your setup by trying to import Cocoa in python. In my case, that didn’t work until I installed two packages:
pip install pyobjc-core
pip install pyobjc-framework-cocoa

I didn’t find these with conda on conda-forge so it was a job for pip. Once these are installed, you should be able to import Cocoa and import Foundation in python. Next, it’s time to check for py2app. This step was new to me (I haven’t used py2app before), but I figured it would work best to have this installed in the python environment I plan to use, so once again, let pip do the work: pip install py2app. After that, you should be able to run py2applet on the command line and it will return a default help message. We’ll use this utility to create a setup.py file for the application, and bundle it.

First, put the following application code into a file named SimpleXibDemo.py:

from Cocoa import *
from Foundation import NSObject

class SimpleXibDemoController(NSWindowController):
    counterTextField = objc.IBOutlet()

    def windowDidLoad(self):
        NSWindowController.windowDidLoad(self)

        # Start the counter
        self.count = 0

    @objc.IBAction
    def increment_(self, sender):
        self.count += 1
        self.updateDisplay()

    @objc.IBAction
    def decrement_(self, sender):
        self.count -= 1
        self.updateDisplay()

    def updateDisplay(self):
        self.counterTextField.setStringValue_(self.count)

if __name__ == "__main__":
    app = NSApplication.sharedApplication()

    # Initiate the contrller with a XIB
    viewController = SimpleXibDemoController.alloc().initWithWindowNibName_("SimpleXibDemo")

    # Show the window
    viewController.showWindow_(viewController)

    # Bring app to top
    NSApp.activateIgnoringOtherApps_(True)

    from PyObjCTools import AppHelper
    AppHelper.runEventLoop()

Next, we can start to package this app by creating a setup.py file using py2applet:

py2applet --make-setup SimpleXibDemo.py

Notice that you call this with the name of the python application file. The setup.py file generated should look like:

"""
This is a setup.py script generated by py2applet

Usage:
    python setup.py py2app
"""

from setuptools import setup

APP = ['SimpleXibDemo.py']
DATA_FILES = []
OPTIONS = {}

setup(
    app=APP,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)

You’ll want to associate the application with an xib file (i.e. the GUI side of things). Just add the filename (although we haven’t made the actual file yet):

DATA_FILES = ['SimpleXibDemo.xib']

Just be sure to name your XIB file accordingly when the time comes (that step is coming soon).

Now it’s time to fire up Xcode. I’m using Xcode 8, on macOS Sierra, things should be similar on earlier versions, but Xcode has evolved a bit over the years. I still have the Welcome widget enabled on Xcode, so the quickest way to get what I wanted was to “create a new Xcode project”, and choose “cross-platform” and “Empty”. We don’t need much in this project, but make sure to save it in the same folder where you created setup.py and SimpleXibDemo.py.

Next, create a new xib file for the app window. The menu path is File→New→File… and under macOS in the User Interface category pick Window. For a filename, type in SimpleXibDemo and it should add the .xib extension. Next, add your .py file to the listing for Xcode: File→Add files to “Project”… Select your file SimpleXibDemo.py. If you open this file in Xcode, you’ll see some decorators that let Xcode know how you will interface with it: @objc.IBAction indicates a function that you want to receive an action. You’ll also notice that the python script instantiates an outlet for the interface and calls it counterTextField: counterTextField = objc.IBOutlet() This will be the recipient of actions within the script.

To lay out the GUI, drag three buttons onto the window from the Object Library (lower right side by default). You can change the text that appears on the button by double clicking.

Next, we’ll associate the quit button with the terminate action. Ctrl-drag from the button over to the FirstResponder. In the pulldown, choose terminate, this will associate the quit button with ending the program.

Next, we have to associate the window with the python class that it represents, and link the buttons to the appropriate IBActions. Note that it is important the class specified in the File’s Owner matches the python class in the .py file. In the code above, that is SimpleXibDemoController so click on the top icon to the left of the workspace (File’s Owner) and then choose the Identity Inspector (likely the third-from-left in the right-side pane). Enter the class name in the top field (Class). This associates the window with the class that will handle actions. Finally, Ctrl-drag from the “-” button to the File’s Owner and select the decrement action. Do the same from the “+” button but pick the increment action. This connects the two buttons to the corresponding class functions.

 

 

Finally (as shown below), add a label to the center of the window and ctrl-drag from the File’s Owner to the label. In the pulldown, choose counterTextField (which should be the only option). This links the label in the GUI to the counterTextField in the python code.

label

Save the .xib file and we’re ready to build the app. From the command line, run:

python setup.py py2app -A

You can then run it from the command line as: ./dist/SimpleXibDemo.app/Contents/MacOS/SimpleXibDemo

Note: it may not work just yet. I had to change the setup.py file to point to the right python executable (this comes from using a local anaconda python instead of the built-in Framework python).

"""
This is a setup.py script generated by py2applet

Usage:
    python setup.py py2app
"""

from setuptools import setup

APP = ['SimpleXibDemo.py']
DATA_FILES = ['SimpleXibDemo.xib']

OPTIONS = {'argv_emulation': True,
           'plist': {
               'PyRuntimeLocations': [
                '/Users/dawe7269/anaconda3/lib/libpython3.6m.dylib'
               ]
           }}

setup(
    app=APP,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)

Where I added the plist to the OPTIONS variable and made sure to provide the path to libpython3.6m.dylib (select the path to the dylib that matches your runtime python version). If you use anaconda, then it should be similar to the path above though your username is probably different.

With this change, run
python setup.py py2app -A
once more, and then try the app again:
./dist/SimpleXibDemo.app/Contents/MacOS/SimpleXibDemo

I’m pretty new to all this, but feel free to let me know if you have trouble getting it to work and I’ll do my best to help.

19 thoughts on “Python with a Cocoa GUI on macOS

  1. Hello Its Good example for python with Cocoa. But what if I want to image in window with my python code in background?

    Reply
  2. Pingback: New top story on Hacker News: Python with a Cocoa GUI on MacOS – World Best News

  3. Pingback: New top story on Hacker News: Python with a Cocoa GUI on MacOS – Hckr News

  4. Pingback: New top story on Hacker News: Python with a Cocoa GUI on MacOS – Golden News

  5. Pingback: New top story on Hacker News: Python with a Cocoa GUI on MacOS – Outside The Know

  6. Pingback: New top story on Hacker News: Python with a Cocoa GUI on MacOS – News about world

  7. Pingback: New top story on Hacker News: Python with a Cocoa GUI on MacOS – Latest news

  8. Pingback: Python with a Cocoa GUI on MacOS – Hacker News Robot

  9. Pingback: Python with a Cocoa GUI on macOS | Infozonic

  10. Pingback: Python with a Cocoa GUI on MacOS | Business Link Global

  11. Pingback: ICYMI: Python snakes its way on the SparkFun SAMD21 Mini, Hackaday.io, 10k thanks, and Tim’s magazine #Python #Adafruit #CircuitPython @circuitpython @micropython @ThePSF @Adafruit « Adafruit Industries – Makers, hackers, artists, designers

  12. Pingback: ICYMI: Python snakes its way on the SparkFun SAMD21 Mini, Hackaday.io, 10k thanks, and Tim’s magazine #Python #Adafruit #CircuitPython @circuitpython @micropython @ThePSF @Adafruit « Adafruit Industries – Makers, hackers, artists, designers

  13. Pingback: ICYMI: Python snakes its way on the SparkFun SAMD21 Mini, Hackaday.io, 10k thanks, and Tim’s magazine #Python #Adafruit #CircuitPython @circuitpython @micropython @ThePSF @Adafruit « Adafruit Industries – Makers, hackers, artists, designers

  14. Pingback: Trouble assembling a XIB file with a python script to export and application macOS Xcode – Ask python questions

  15. Pingback: Having troubles pioneering using py2app in Xcode, can’t open a link in a browser with buttons – Ask python questions

  16. Pingback: Add 1 more parameter to class returns TypeError: … the metaclass of a derived class must be a (non-strict) – Ask python questions

  17. Hi,
    nice article, but some improvements I would suggest:
    When already bundled it will be hard to debug the application.
    So with XCode 12 at least you have to export the xib file as Cocoa .nib file.
    Then change as follows:
    if __name__ == “__main__”:
    app = NSApplication.sharedApplication()
    path = os.path.dirname(__file__) + “/SimpleXibDemo.nib”
    # Initiate the contrller with a XIB
    viewController = SimpleXibDemoController.alloc()
    #viewController.initWithWindowNibName_(“SimpleXibDemo”)
    viewController.initWithWindowNibPath_owner_(path, viewController)
    # Show the window
    viewController.showWindow_(viewController)

    # Bring app to top
    NSApp.activateIgnoringOtherApps_(True)

    from PyObjCTools import AppHelper

    AppHelper.runEventLoop()

    Reply
  18. Hi, Andrews. A can not associate window with python class at Xcode 13. Not choice with increment an decrement functions

    Reply
    • Can you explain more about what isn’t working? I haven’t done anything with this recently, but I do have xcode 13 and will try repeating my steps to see if things need to be updated.

      Reply

Leave a comment