Here's a first for this website: a real, (hopefully) useful desktop application for you to take home and play with!
Here it is, all zipped up and ready to go. See the enclosed README.txt for wise words regarding how to use it.
I had the brilliant idea of using my MFC device (an HP OfficeJet 7410) to scan multiple happy snaps in one go. While this idea
would undoubtedly save me scanning time, it leaves the problem of how to extract these multiple images from a single scan, such as the one shown here:
You can see the major problem with this idea: the various 'real' images will be placed on the scanner bed haphazardly and so will be rotated at odd angles. Processing with a 'normal' application (such as the excellent Paint.NET) would require too much grunt work for my taste, quickly leading to boredom and possible RSI.
I couldn't find anything out there in the GoogleVerse that did what I wanted so I took this as a challenge and did what any self-respecting programming nerd would do: wrote my own application. 'GT' lets me select the boundary of an
embedded image within a scan file, and then extract and automatically rotate the desired image ready for saving as a PNG file.
Since I am quickly becoming a GR8 (Groovy/Grails/Griffon/Gant/…) tragic, Griffon was the natural choice for this little project.
Although it is early days yet for the project, Griffon aims to bring the same "Configuration by Convention" goodness to desktop GUI development that Grails has brought to Web development. In fact, Griffon 'borrows' heavily from Grails:
many of the build scripts, etc. are straight carry-overs, meaning that anybody familiar with Grails can get started easily.
This is all that is needed to get started:
griffon create-app GT
cd GT
griffon run-app
These three shell commands give a simple immediately runnable application:
As with Grails, Griffon imposes a clear MVC structure for the code and also creates a standrd project filesystem heirarchy:
Simplicity is the watchword, as can be seen by looking at the code statistics for the finished application:
Note that the bulk of the application lies in the 3rd party Java graphics-processing code and this is what inflates the lines of code statistics.
While I'm here, a big "thank you" is due to prometheuzz for making the code for the "Rotating Calipers" algorithm available (see the Java package bk.geom.rotatingcalipers). This algorithm is used to find the bounding box of a polygon, regardless of whether that polygon is absolutely aligned
with the XY axes or not. There is a good demonstration applet of this at http://www.iruimte.nl/calipers/.
Enough background. Time to take a short ramble through (the most important parts of) the MVC triad…
models/GTModel.groovy
import groovy.beans.Bindable
import javax.swing.ImageIcon
class GTModel {
[...elided...]
@Bindable boolean saveAsEnabled = false
@Bindable String file
@Bindable ImageIcon image
@Bindable ImageIcon destinationImage
@Bindable String labelText = 'Welcome!'
}
In a typical Swing application, bits of model typically get spread through various components. By bringing everything together, Griffon makes it much easier to understand what's what.
This excerpt also shows one of the nicest things about Griffon: the @Bindable annotation, which encapsulates the observerable/observer pattern. We'll come back to this in a while.
views/GTView.groovy
Simple! A thing of beauty…
import static java.awt.BorderLayout.*
import javax.swing.JSplitPane
import java.awt.Color
build(GTActions)
application(title: 'GT',
pack: true,
locationByPlatform: true,
iconImage: imageIcon('/griffon-icon-48x48.png').image,
iconImages: [imageIcon('/griffon-icon-48x48.png').image,
imageIcon('/griffon-icon-32x32.png').image,
imageIcon('/griffon-icon-16x16.png').image]
) {
menuBar(build(GTMenuBar))
panel(border: emptyBorder(6)) {
borderLayout()
splitPane(id: "splitter",
preferredSize: [1024, 768],
dividerLocation: 512,
orientation: JSplitPane.HORIZONTAL_SPLIT,
constraints: CENTER,
border: lineBorder(color: Color.BLACK, thickness: 1)) {
panel(id: 'sourceImagePanel') {
borderLayout()
build(GTSourceImagePanel)
}
panel(id: 'destinationImagePanel') {
borderLayout()
build(GTDestinationImagePanel)
}
}
label(id: 'status',
constraints: SOUTH,
border: emptyBorder(4),
text: bind { model.labelText })
}
}
For those familiar with plain Java Swing, this should be a complete breath of fresh air.
Very little glue code is required here; the Swing containment hierarchy is clear and the layout policies are easy to see.
Some may be happy to see that there are no anonymous inner classes in use: Groovy's closures allow for much a cleaner implementation. I've been teaching Java since about 1998 and I have observed that course participants often have
trouble 'trusting' anonymous inner classes. Even though they are custom-designed for the sort of glue code required to join a button and its action together, I have observed that many participants' first instinct is to avoid them. It
hasn't helped that many IDEs haven't worked well with them, either. Hopefully, closures will be more easily comprehended and thus better accepted.
The 'status' label shows how the model.labelText is 'automagically' bound to the 'text' property in the label component. Any changes to model.labelText (as will be seen in the controller code, later) will be reflected in the label's
text value. Once again, this substantially helps to cleans up the code.
Griffon allows complex code to be broken into sub-scripts that are then included via the build() method. This technique is used to isolate action definitions and menu definitions and also to "break down" the various panel content GUIs
into separate files for ease of comprehension.
Griffon allows Swing-style action definitions to be streamlined, as shown in this excerpt from views/GTActions.groovy:
actions {
action(id: 'saveAsAction',
name: 'Save As...',
mnemonic: 'S',
smallIcon: builder.imageIcon("/disk.png"),
accelerator: shortcut('S'),
enabled: bind { model.saveAsEnabled },
closure: controller.saveAsActionClosure
)
action(id: 'mirrorYAction',
name: 'Mirror on Y Axis',
smallIcon: builder.imageIcon('/shape_flip_vertical.png'),
enabled: bind { model.saveAsEnabled },
closure: controller.mirrorYActionClosure
)
[...elided...]
}
It is easy to see the Griffon 'goodness' here: the use of closures, the use of bindings, the use of the GUI builder facilies (to obtain an appropriate icon resource), and shortcut handling. Taken together, this gives a clean,
declarative style that I find very valuable.
(By the way, for plain Swing, the Swing Action Manager provides somewhat similar capabilities…).
The use of the actions can be seen in this excerpt from views/GTMenuBar.groovy:
import static griffon.util.GriffonApplicationUtils.*
menuBar = menuBar {
if (!isMacOSX) {
menu(text: 'File', mnemonic: 'F') {
menuItem(exitAction)
}
}
menu(text: 'Source', mnemonic: 'S') {
menuItem(openAction)
separator()
menuItem(extractAction)
menuItem(clearAction)
}
[...elided...]
}
return menuBar
It is also interesting to see that Griffon makes it easy to keep the various platform-specific foibles in mind.
To round off this tour through the view's code, here is the views/GTDestinationImagePanel.groovy file:
import static java.awt.BorderLayout.*
import javax.swing.JLabel
import java.awt.Color
import org.jdesktop.swingx.painter.PinstripePainter
jxheader(title: "Destination",
description: 'Display and manipulate the isolated portion of the source image.',
border: emptyBorder(4),
constraints: NORTH)
scrollPane(id: 'scrollPane',
constraints: CENTER) {
jxlabel(id: 'destination',
icon: bind { model.destinationImage },
constraints: CENTER,
horizontalAlignment: JLabel.CENTER_ALIGNMENT,
border: loweredBevelBorder(),
backgroundPainter: new PinstripePainter(Color.LIGHT_GRAY))
}
hbox(constraints: SOUTH, border: emptyBorder(4)) {
button(rotate90LeftAction, text: '')
hstrut(8)
button(rotate90RightAction, text: '')
hstrut(8)
button(mirrorXAction, text: '')
hstrut(8)
button(mirrorYAction, text: '')
hglue()
button(saveAsAction)
}
The most interesting aspect of this code is the way that the facilities made available by the use of the SwingXBuilder Plugin (jxheader, jxlabel and the PinstripePainter background painter) are seamlessly incorporated into the view.
This is really quite pretty code; it would be beautiful but for the various import statements. I'd like Griffon (or perhaps Groovy itself: there's something similar
afoot with respect to Grape, but this isn't quite the same thing…) to do something about them…they seem somehow 'ugly' and don't really add much to things.
controllers/GTController.groovy
This is the third major part of the MVC triumvirate.
Here's a fairly cut-down overview of the controller:
class GTController {
def model
def view
def builder
def saveAsActionClosure = {evt = null ->
def file = selectFileForSave()
if (!file)
return
ImageIO.write(view.destination.icon.image, "png", file)
model.labelText = "Saved as $file.name..."
}
def mirrorYActionClosure = {evt = null ->
doOutside {
def img = toBufferedImage(model.destinationImage.image)
AffineTransform transform = AffineTransform.getScaleInstance(1, -1)
transform.translate(0, -img.height)
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR)
img = op.filter(img, null)
edt {
model.destinationImage = new ImageIcon(img)
}
}
}
def exit = {evt = null ->
app.shutdown()
}
[...elided...]
}
Things worthy of notice here include: the injection of the builder, model and view components (note that views get the builder implicitly), the action closures (that are referenced from the actions shown earlier) and the 'exit' closure
that directly references the implictly available 'app' instance.
It is also worth noting the assignment to the bound property 'model.labelText.'
Of particular note is the way that Griffon eases the burden of managing threading. Swing threading is (sadly!) probably the most misunderstood and mis-applied aspect of the whole of Java. Unfortunately, it is also the area that has the
most effect on making an application 'feel' good. Bob's rule for Swing GUIs is: "GUI updates have to be on the Event Dispatch Thread (EDT), but you must never do any long-running non-GUI-related task on the EDT." Each action is executed on
the EDT, in true Swing fashion. Griffon makes it easy to follow Bob's rule by providing the doOutside method that allows the single Swing GUI thread to remain 'live' while the actual work is done in another (background) thread. Griffon
also supplies the edt method for when things (like GUI updates) simply have to be done on the EDT, and doLater which allows Swing to decide when it is free enough to handle a long-running GUI-updating activity.
There's more on Griffon and threading here.
Other
There's a lot more Good Stuff that Griffon gives you. For example, the Griffon Quick Start says:
The run-app script implies the execution of the package script. The package script creates file artifacts suitable for a Java application, a WebStart application, and an Applet, with code signed by a self-signed certificate
That's a significant piece of added value just there!
Griffon's plugins scheme is also a Real Good Idea and there are an ever-growing number of plugins to be had, from the SwingXBuilder plugin that I have used here, through to things like a standard "splash screen" facility and various
testing/code coverage tools.
Don't forget the i18n support and standardized resource handling!
And don't forget testing…although I haven't actually used it in for this application (bad boy, Bob!), Griffon generates placeholders for integration/unit testing.
Griffon also creates an ant script (not Gant, shame!) for the project.
I may end up eating my words in future (wouldn't be the first time!) as Flash/Flex and Silverlight fade into obscurity, but I feel fairly safe saying that IMHO, Griffon beats the ill-conceived (and probably ill-fated) JavaFX technology hands-down. Even though JavaFX is technologically quite good, I'm not betting my house on it. In any case, the use of one doesn't preclude the use of the other, as this posting (to take but one simple example) shows.
Despite all protestations to the contrary, the JSR 269 Swing AppFramework work seems stillborn.
Griffon seems to me to have a much rosier future ahead of it than either JavaFX or AppFramework. If you haven't already done so, ride the Griffon, you won't be sorry!