It has been quite a while since the first release of CoreGTK back in August 2014 and in that time I’ve received a lot of very good feedback about the project, what people liked and didn’t like, as well as their wishlists for new features. While life has been very busy since then I’ve managed to find a little bit of time here and there to implement many of the changes that people were hoping for. As mentioned in my previous post here are the highlighted changes for this new version of CoreGTK:
Move from GTK+ 2 to GTK+ 3
GTK+ 3 is now the current supported widget toolkit and has been since February 2011. Now that GTK+ 3 is supported on all platforms (Windows, Mac and Linux) it makes sense to move over and take advantage of the updated features.
Additionally this allows for a natural break in compatibility with the previous release of CoreGTK. What that means for the end user is that I currently don’t have any plans on going back and applying any of these new ideas/changes to the old GTK+ 2 version of the code base, instead focusing my time and effort on GTK+ 3.
Prefer the use of glib data types over boxed OpenStep/Cocoa objects (i.e. gint vs NSNumber)
When originally designing CoreGTK I decided to put a stake in the ground and simply always favour OpenStep/Cocoa objects where possible. The hope was that this would allow for easier integration with existing Objective-C code bases. Unfortunately good intentions don’t always work out in the best way. One of the major pieces of feedback I got was to take a less strict approach on this and drop the use of some classes where it makes sense. Specifically keep using NSString instead of C strings but stop using NSNumber in place of primitives like gint (which itself is really just a C int). The net result of this change is far less boilerplate code and faster performance.
So instead of writing this:
/* Sets the default size of the window */ [window setDefaultSizeWithWidth: [NSNumber numberWithInt:400] andHeight: [NSNumber numberWithInt:300]];
you can now simply write this:
/* Sets the default size of the window */ [window setDefaultSizeWithWidth: 400 andHeight: 300];
Base code generation on GObject Introspection instead of a mix of automated source parsing and manual correction
The previous version of CoreGTK was, shall we say, hand crafted. I had written some code to parse header files and generate a basic structure for the Objective-C output but there was still quite a bit of manual work (days/weeks/months) involved to clean up this output and make it what it was. Other than the significant investment in time required to make this happen it was also prone to errors and would require starting back at square one with every new release of GTK+.
This time around the output is generated using GObject Introspection, specifically by parsing the generated GIR file for that library with the new utility CoreGTKGen. The proccess of generating new CoreGTK bindings using CoreGTKGen now takes just a couple of seconds and produces very clean and simple source code files. This is also really just the start as I’m sure there are plenty of improvements that can be made to CoreGTKGen to make it even better! Perhaps equally exciting is that once this process is perfected it should be relatively easy to adapt it to support other GObject Introspection supported libraries like Pango, Gdk, GStreamer, etc.
Let’s have an example shall we?
While there are a couple of good examples over at the Getting Started page of the Wiki and even within the CoreGTK repo itself I figured I would show something different here. It has always been my goal with this project to make it as easy as possible for existing Objective-C users to port their applications to GTK+. Perhaps you were previously using a widget toolkit like Cocoa on the Mac and now you want to release your application on more platforms. What better way than to keep your existing business logic and swap out the GUI (you do practice good MVC right? :P).
So going with this idea here is a tutorial of porting the “Start Developing Mac Apps Today” example from Apple’s developer website here. This application is incredibly simplistic but basically lets you set a “volume” value either by typing in a number in the text box at the top, moving the slider up and down, or pressing the Mute button. Regardless of which action you take the rest of the GUI is updated to match.
Step 1) Setup the GUI
For this I will be using GLADE as a replacement for the Xcode Interface Builder but you could always program your GUI by hand as well.
From the Apple website we are trying to re-create something that looks like this:
Thankfully in GLADE this is relatively easy and I was able to do a quick and dirty mock up resulting in this:
Step 2) Configure GUI signals (i.e. events)
GLADE also makes this easy, simply click on the widget, flip over to the Signals tab and type in your handler name.
Here are the ones I created:
- window (GtkWindow)
- Signal: destroy
- Handler: endGtkLoop
- entry (GtkEntry)
- Signal: changed
- Handler: takeValueForVolume
- scale (GtkScale)
- Signal: value-changed
- Handler: sliderValueChanged
- mute_button (GtkButton)
- Signal: clicked
- Handler: muteButtonClicked
Step 3) Create classes
Even though Cocoa and GTK+ don’t map exactly the same I decided to follow Apple’s conventions where it made sense just for consistency.
AppDelegate.h
#import "CoreGTK/CGTKEntry.h" #import "CoreGTK/CGTKScale.h" #import "Track.h" @interface AppDelegate : NSObject { CGTKEntry *textField; CGTKScale *slider; Track *track; BOOL updateInProgress; } @property (nonatomic, retain) CGTKEntry *textField; @property (nonatomic, retain) CGTKScale *slider; @property (nonatomic, retain) Track *track; /* Callbacks */ -(void)mute; -(void)sliderChanged; -(void)takeValueForVolume; /* Methods */ -(void)updateUserInterface; -(void)dealloc; @end
AppDelegate.m
#import "AppDelegate.h" @implementation AppDelegate @synthesize textField; @synthesize slider; @synthesize track; /* Callbacks */ -(void)mute { if(!updateInProgress) { updateInProgress = YES; [self.track setVolume:0.0]; [self updateUserInterface]; updateInProgress = NO; } } -(void)sliderChanged { if(!updateInProgress) { updateInProgress = YES; [self.track setVolume:[self.slider getValue]]; [self updateUserInterface]; updateInProgress = NO; } } -(void)takeValueForVolume { NSString *text = [self.textField getText]; if([text length] == 0) { return; } if(!updateInProgress) { updateInProgress = YES; double newValue = [[self.textField getText] doubleValue]; [self.track setVolume:newValue]; [self updateUserInterface]; updateInProgress = NO; } } /* Methods */ -(void)updateUserInterface { double volume = [self.track volume]; [self.textField setText:[NSString stringWithFormat:@"%1.0f", volume]]; [self.slider setValue:volume]; } -(void)dealloc { [textField release]; [slider release]; [track release]; [super dealloc]; } @end
Track.h
/* * Objective-C imports */ #import <Foundation/Foundation.h> @interface Track : NSObject { double volume; } @property (assign) double volume; @end
Track.m
#import "Track.h" @implementation Track @synthesize volume; @end
Step 4) Wire everything up
In order to make everything work, load the GUI from the .glade file, connect the signals to the AppDelegate class, etc. we need some glue code. I’ve placed this all in the main.m file.
main.m
int main(int argc, char *argv[]) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; /* This is called in all GTK applications. Arguments are parsed * from the command line and are returned to the application. */ [CGTK autoInitWithArgc:argc andArgv:argv]; /* Create a builder to load GLADE file */ CGTKBuilder *builder = [[CGTKBuilder alloc] init]; if([builder addFromFileWithFilename:@"mac_app.glade" andErr:NULL] == 0) { NSLog(@"Error loading GUI file"); return 1; } /* Create an AppDelegate to link to the GUI */ AppDelegate *appDelegate = [[AppDelegate alloc] init]; /* Get text field, wrapping returned Widget in new CGTKEntry */ appDelegate.textField = [[[CGTKEntry alloc] initWithGObject:(GObject*)[[CGTKBaseBuilder getWidgetFromBuilder:builder withName:@"entry"] WIDGET]] autorelease]; /* Get slider, wrapping returned Widget in new CGTKScale */ appDelegate.slider = [[[CGTKScale alloc] initWithGObject:(GObject*)[[CGTKBaseBuilder getWidgetFromBuilder:builder withName:@"scale"] WIDGET]] autorelease]; /* Create track class for AppDelegate */ Track *track = [[Track alloc] init]; appDelegate.track = [track autorelease]; /* Pre-synchronize the GUI */ [appDelegate updateUserInterface]; /* Use signal dictionary to connect GLADE signals to Objective-C code */ NSDictionary *dic = [[NSDictionary alloc] initWithObjectsAndKeys: [CGTKCallbackData withObject:[CGTK class] andSEL:@selector(mainQuit)], @"endGtkLoop", [CGTKCallbackData withObject:appDelegate andSEL:@selector(mute)], @"muteButtonClicked", [CGTKCallbackData withObject:appDelegate andSEL:@selector(sliderChanged)], @"sliderValueChanged", [CGTKCallbackData withObject:appDelegate andSEL:@selector(takeValueForVolume)], @"takeValueForVolume", nil]; /* CGTKBaseBuilder is a helper class to maps GLADE signals to Objective-C code */ [CGTKBaseBuilder connectSignalsToObjectsWithBuilder:builder andSignalDictionary:dic]; /* Show the GUI */ [[CGTKBaseBuilder getWidgetFromBuilder:builder withName:@"window"] showAll]; /* * Release allocated memory */ [builder release]; /* All GTK applications must have a [CGTK main] call. Control ends here * and waits for an event to occur (like a key press or * mouse event). */ [CGTK main]; /* * Release allocated memory */ [appDelegate release]; [pool release]; // Return success return 0; }
Step 5) Compile and run
So while this is a very basic, quick and dirty example it does prove the point. As for CoreGTK this release is still under development as I try and flush out any remaining bugs but please give it a shot, submit issues or pitch in to help if you’re interested! You can find the CoreGTK project at http://coregtk.org.
Example Source Code | |
File name: | mac_port_example.zip |
File hashes: | Download Here |
License: | (LGPL) View Here |
File size: | 5.3KB |
File download: | Download Here |
Leave a Reply