Creating an Interface for an Application

Making your package into a GUI

Dale R. Horton
Wolfram Research
daleh@wolfram.com

Abstract

The front end has many features which can make packages more accessible to users. Most packages are driven by kernel commands and their front end interface (if one exists) consists solely of buttons that paste and evaluate kernel commands in a notebook. A largely unused strategy is to use the palettes as a Graphical User Interface(GUI) to the functionality of the package. Beginners can start to use the package quickly. Advanced users can easily access commonly used functionality. If properly implemented, the palette makes using the package easier, and the package makes using the palette more powerful.

The basic idea.

For a traditional package, the front end only acts as a place to store, typeset, and edit kernel commands. The user is really interfacing with the kernel through a series of In's and Out's. The front end is only acting as a storage device for In[]s, Out[]s, and side-effects.

With active button elements and front end commands, packages can be accessed by users through a completely different interface.

The Basics.

The building block of a front end interface is the button. If you want to use buttons to enhance your packages, they must be able to activate package functions and other kernel commands. The behavior of a button is controlled through its options.

The ButtonFunction option specifies the action of the button. It will contain any kernel commands you want the button to execute.

The processor of the ButtonFunction option is determined by the ButtonEvaluator option. The default is ButtonEvaluator->None, which allows only commands that the front end can execute. To use the default kernel, set ButtonEvaluator->Automatic. You will also need Active->True to make the button active.

Cell[BoxData[
    ButtonBox["Test",
      ButtonFunction:>Print[ "Print Test"],
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

Another important point is that the front end does not understand full kernel syntax. This means that the ButtonFunction must be in FullForm (except for a few functions). Fortunately, buttons created by kernel commands (like CellPrint and NotebookWrite) will convert the ButtonFunction automatically.

Input.

None/Embedded

There can be no input to the button (as demonstrated above) or the input can be separate but coded into the button. The ButtonFunction can be a pure function of one or two arguments. Arguments can be supplied from a variety of sources, determined by the ButtonSource and ButtonData options. The most common methods are to use the ButtonContents or the ButtonData.

Cell[BoxData[
    ButtonBox["Test",
      ButtonFunction:>(Print[#]&),
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

Cell[BoxData[
    ButtonBox["Test",
      ButtonData:>"ButtonData Test",
      ButtonFunction:>(Print[#]&),
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

Do not mistake this for evaluation. The front end simply does a replacement, which can occasionally cause trouble.

Cell[BoxData[
    ButtonBox["Test",
      ButtonFunction:>Print,
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

Cell[BoxData[
    ButtonBox["Test",
      ButtonFunction:>(Map[Print[#]&,{1,2,3,#}]&),
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

Simple/Immediate

In many cases, the user may need to specify the input for the button. The simplest method is to use Input or InputString and convert to an expression with MakeExpression or ToExpression.

expr = MakeExpression@InputString["What is the input?"]; 
If[Head@expr === ErrorBox,  $Failed,  ReleaseHold@expr]

Input can also be read from a cell with NotebookFind and NotebookRead.

Cell[BoxData[
    RowBox[{"2", "+", "2"}]], "Input",
  Deletable->False,
  CellTags->"Input"]

f:=Module[{nb = 
            ButtonNotebook[]}, 
NotebookFind[
          nb, "Input",  All,  CellTags]; 
Print[
          ToExpression@First@
              NotebookRead[nb]];
]

Cell[BoxData[
    ButtonBox["Evaluate",
      ButtonFunction:>f,
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

Stored/Delayed

When constructing a dialog (where one buttons controls the action of another), there must be a method of registering or storing the state of the different parts of the dialog. The effect of an action (by pressing button A) is then delayed until the dialog is completed  (by pressing button B). There are two places you can store the delayed input: the kernel (as a variable) and the front end (embedded in a cell or notebook).

The simplest method is to store the input in a kernel variable by making an assignment in the ButtonFunction. But this has some serious disadvantages. What if the kernel is quit or crashes? What if the user cancels or closes the dialog and doesn't expect any actions taken in the dialog to persist. Instead, it is best to store the data from button A in the dialog and have button B retrieve the data. The retrieval can take the form demonstrated above, or the data can be hidden. A common way of hiding information is to use the TaggingRules of a Cell or Notebook.

f := Module[{nb = ButtonNotebook[], val,  
          sel}, 
sel = 
          NotebookFind[nb, "Change",  All,  CellTags]; 
        val = FullOptions[sel,  TaggingRules]; 
SetOptions[
          sel,  TaggingRules -> (! val)]]

Cell[BoxData[
    ButtonBox[
      RowBox[{"Change", " ", "Value"}],
      ButtonFunction:>f,
      ButtonEvaluator->Automatic,
      Active->True]], "Text",
  TaggingRules:>True,
  CellTags->"Change"]

g := Module[{nb = ButtonNotebook[], val,  
          sel}, 
sel = 
          NotebookFind[nb, "Change",  All,  
            CellTags]; 
val = 
          FullOptions[sel,  TaggingRules]; 
Print[
          val]]

Cell[BoxData[
    ButtonBox[
      RowBox[{"Get", " ", "Value"}],
      ButtonFunction:>g,
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

Output.

Redirecting output.

The output of a ButtonFunction is not directed into any notebook, only side-effects of evaluations will be visible. Functions like Print and Plot will display their side effects, but the precise location of the side effect is difficult to control.

Instead, output should be displayed by selecting a location with NotebookFind and NotebookWrite. If you are writing an entire notebook, then NotebookPut is sufficient.

MakeBoxes and ToBoxes are very useful tools when constructing typeset cells from expressions.

Cell[" ", "Text",
  CellTags->"Output"]

nb = EvaluationNotebook[]; 
NotebookFind[nb, "Output",  All,  CellTags]; 
    NotebookWrite[
      nb, {Cell[BoxData@MakeBoxes[2 + 2],  "Input"], 
        Cell[BoxData@ToBoxes[2 + 2],  "Output"]}];

Redirecting graphics.

Displaying graphics is based on the same principle. DisplayString is used for graphics much like ToBoxes is used for output. Note that DisplayFunction->Identity is used to suppress the normal side-effect of Plot.

gr  =  
      Plot3D[Sin[x y], {x, 0, Pi}, {y, 0, Pi}, 
        DisplayFunction -> Identity]; 
nb = 
      NotebookPut[
        Notebook[{Cell[
              GraphicsData["PostScript", 
                DisplayString[gr]], "Graphics",  
              ShowCellBracket -> False,  
              ImageMargins -> {{0, 0}, {0, 0}}]}, 
          WindowSize -> {FitAll, FitAll}, 
          WindowElements -> {}]];

NotebookClose[nb]

Redirecting errors.

What do you do when your kernel function generates an error message? When a kernel function is evaluated in a notebook, the error normally appears in the same notebook after the input. When evaluated from a ButtonFunction, error messages will appear somewhere in the button's notebook, perhaps mangling your carefully designed palette or dialog.

This is one of the trickiest problems to fix, and there are many different strategies. One method is to set the front end option MessageOptions.

Solve[2];

\(\(Module[{oldopts}, 
{oldopts} = 
          Options[$FrontEnd, 
            MessageOptions]; 
SetOptions[$FrontEnd, 
          MessageOptions -> {"KernelMessageAction" -> {"Beep", "PrintToConsole"}}]; 
Solve[
          2]; 
SetOptions[$FrontEnd, \ 
          oldopts]
];\)\)

Another method is to temporarily reassign $Messages. Messages can be detected with $MessageList.

Block[{$Messages}, 
Solve[
        2]; 
$MessageList
]

Packages.

To this point we've discussed how to use the kernel in button functions, but not why. It is pretty clear that the kernel can do things that the front end cannot, but one could ask why use a package instead of built-in functions in buttons.

The strongest arguments are the same ones used for justifying any package. Packages give us a way to easily control  complex computations and actions. Not only is it true for the underlying computations, but is also true for controlling the input and output of ButtonFunctions. In this way, you can use package functions to make the buttons in your palettes more powerful.

Palette Editing.

It is much easier to edit and debug a package function than the ButtonFunction. Not only is the ButtonFunction deeply embedded in a palette, but it is formatted in the unwieldy FullForm.

f := Print["test1"]

Cell[BoxData[
    ButtonBox["Evaluate",
      ButtonFunction:>f,
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

f := Print["test2"]

A helpful hint: Redirect messages to the console (see above). This prevents error messages from cluttering up your palettes.

Package friendly features.

There are many kernel features that are much easier to accomplish when you create functions. Things such as reusability, modularity, function overloading, and recursive evaluation. These features are particularly useful when writing buttons that replace themselves.

MyCell[val_]  :=  
      Cell[BoxData@
          ButtonBox[ToBoxes@val, ButtonEvaluator -> Automatic, 
            Active -> True,  
            ButtonFunction :> 
Module[{nb = 
                    ButtonNotebook[]}, 
SelectionMove[nb,  
                  All,  ButtonCell]; 
NotebookWrite[nb, 
                  MyCell[! val]]]], "Text"]

CellPrint[MyCell[True]]

True

Autoloading and Shadowing.

To remove the need for manually loading a package and to gracefully recover from kernel crashes, buttons should load any necessary packages with Needs. However, there is a complication called shadowing.

Needs["Graphics`Graphics`"]; LogPlot[E^x, {x, 0, 1}]

The kernel parses expressions before evaluating. During the parsing phase, the kernel creates a symbol in the current context (in this case Global`) for all symbols that do not currently exist. This Global` symbol conflicts with the package symbol. There are different ways to workaround this limitation, but the best method is to use the full context name.

Quit

Needs["Graphics`Graphics`"]; 
    Graphics`Graphics`LogPlot[x, {x, 0, 1}];

For a button created by a kernel command (NotebookWrite, NotebookPut, etc.), there is an extra complication. The front end does not understand contexts. It simply writes whatever symbol the kernel provides it.

CellPrint@
      Cell[BoxData@
          ButtonBox["plot", Active -> True,  
            ButtonEvaluator -> Automatic, 
            ButtonFunction :> (Needs["Graphics`Graphics`"]; 
                Graphics`Graphics`LogPlot[
                  x, {x, 0, 1}])],  "Text"]

Cell[BoxData[
    ButtonBox["plot",
      ButtonFunction:>CompoundExpression[ 
          Needs[ "Graphics`Graphics`"], 
          LogPlot[ x, {x, 0, 1}]],
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

There is a kernel variable, $ContextPath, that determines which symbols will be recognized by their short names. If a package has already been loaded, all public symbols will use short names. Private symbols will always use long names. We can convince the kernel to provide the full name for public symbols by temporarily manipulating $ContextPath.

\(Block[{$ContextPath = {"System`"}}, 
      CellPrint@
        Cell[BoxData@
            ButtonBox["plot", Active -> True, \ 
              ButtonEvaluator -> Automatic, 
              ButtonFunction :> \((Needs["Graphics`Graphics`"]; 
                  Graphics`Graphics`LogPlot[
                    x, {x, 0, 1}])\)], \ "Text"]]\)

Cell[BoxData[
    ButtonBox["plot",
      ButtonFunction:>CompoundExpression[ 
          Needs[ "Graphics`Graphics`"], 
          Graphics`Graphics`LogPlot[ x, {x, 0, 1}]],
      ButtonEvaluator->Automatic,
      Active->True]], "Text"]

Examples

NotebookDiff

Collide (Written with Jeff Bryant)

Spreadsheet

Summary

Embedding kernel functionality into buttons is a crucial element to creating a GUI for a package. Buttons must be programmed to access the kernel, taking care to properly collect input and direct output. In addition, there are issues specific to using package functions. Put together, package developers are able to create a user-friendly interface to the functionality of the package.

References