How to organise functions inside a package instead of the initialisation of a DynamicModule?

Format-level Approach

There are a few ways to do this. In my mind, the best way to handle this, in my mind, is to use an object-oriented approach. For this we define a function at package level that will build the GUI, but attach it as a Format-form to an exposed symbol. For instance I do that here. A toy example would look like this:

BeginPackage["GUI`"];
(* Exposed functions *)
GUI::usage = "GUI object";
(* Package level functions *)
BeginPackage["`Package`"];
guiImp1::usage = "row 1";
guiImp2::usage = "row 2";
makeGUI::usage = "builds the GUI";
EndPackage[];
(* Implementation *)
Begin["`Private`"];
guiImp1[] := {"lololol"};
guiImp2[] := {"lol2"};
makeGUI[ops : OptionsPattern[]] :=
  DynamicModule[{$packageLoaded},
   Dynamic@
    Framed[
     If[$packageLoaded,
      Grid[{
        guiImp1[],
        guiImp2[]
        },
       Alignment -> Left],
      "GUI package not loaded."
      ],
     ops,
     RoundingRadius -> 5, Background -> GrayLevel[.98], 
     FrameStyle -> GrayLevel[.8]
     ],
   Initialization :>
    {
     $packageLoaded :=
      DownValues[makeGUI] =!= {}
     }
   ];
Format[e : GUI[ops : OptionsPattern[]]] :=
  Interpretation[
   makeGUI[ops],
   e
   ];
End[];
EndPackage[];

This is also nice because it makes it copy-cleanly. For instance if I load that, run GUI[FrameStyle -> Pink] and copy the result it returns GUI`GUI[FrameStyle -> RGBColor[1, 0.5, 0.5]]

Package-protected

We could also drop the Format stuff from that and simply provide makeGUI as the top-level. The important thing to note is that it checks the loading-status of the package. If it's not loaded we don't want it to return a string of errors or pink boxes.

BeginPackage["GUI`"];
(* Exposed functions *)
GUI::usage = "builds the GUI";
(* Package level functions *)
BeginPackage["`Package`"];
guiImp1::usage = "row 1";
guiImp2::usage = "row 2";
EndPackage[];
(* Implementation *)
Begin["`Private`"];
guiImp1[] := {"lololol"};
guiImp2[] := {"lol2"};
GUI[ops : OptionsPattern[]] :=
  DynamicModule[{$packageLoaded},
   Dynamic@
    Framed[
     If[$packageLoaded,
      Grid[{
        guiImp1[],
        guiImp2[]
        },
       Alignment -> Left],
      "GUI package not loaded."
      ],
     ops,
     RoundingRadius -> 5, Background -> GrayLevel[.98], 
     FrameStyle -> GrayLevel[.8]
     ],
   Initialization :>
    {
     $packageLoaded :=
      DownValues[GUI] =!= {}
     }
   ];
End[];
EndPackage[];

Auto-loading

Sometimes you just want a GUI that stores its entire implementation. If we want that we can adapt the either approach pretty trivially using SaveDefinitions. The only place where we need to be sneaky is wrapping our argument in a DynamicModule that doesn't do anything and then using Dynamic@Refresh[..., None] so that the definitions are saved but our function is only called once. For instance we can do:

BeginPackage["GUI`"];
(* Exposed functions *)
GUI::usage = "GUI object";
(* Package level functions *)
BeginPackage["`Package`"];
guiImp1::usage = "row 1";
guiImp2::usage = "row 2";
makeGUI::usage = "builds the GUI";
EndPackage[];
(* Implementation *)
Begin["`Private`"];
guiImp1[] := {"lololol"};
guiImp2[] := {"lol2"};
makeGUI[ops : OptionsPattern[]] :=
  DynamicModule[{$packageLoaded},
   Dynamic@
    Framed[
     Grid[{
       guiImp1[],
       guiImp2[]
       },
      Alignment -> Left],
     ops,
     RoundingRadius -> 5, Background -> GrayLevel[.98], 
     FrameStyle -> GrayLevel[.8]
     ]
   ];
Format[e : GUI[ops : OptionsPattern[]]] :=
  Interpretation[
   DynamicModule[{},
    Dynamic[Refresh[GUI; makeGUI[ops], None]],
    SaveDefinitions -> True
    ],
   e
   ];
End[];
EndPackage[];

And now all *Values for the package will be stored in every GUI object. This is, of course, an expensive approach and should only be used for GUIs with a small number of expected instances.


There are few aspects:

  • gui implementation (I'm about to tackle it)

  • gui generation (see b3m2a1's answer)

  • dependencies management

    (see SaveDefinitions or How can I include functions from a package into a CDF file?)

  • a lot of tiny idioms I'm not ready to show my approach to but they are briefly summarized in e.g.:

    WTC 2017 talk "Building Maintainable Dynamic Interfaces" - Lou D'Andria

About implementation:

The solution to how to make your code more modularized is to pass relevant variables around with e.g.: topRow[Dynamic[view_]]:=.... This is a 'proper way' to do this, the way the WRI advertises. It is fine but can be cumbersome for larger projects.

Notice that Dynamic does not do any magic tricks here, we use it because it is HoldAll. You could use Hold instead or SetAttributes[topRow, HoldAll] and skipp Dynamic. Dynamic is just more readable than Hold and more convenient than attributes approach.

BeginPackage["Test`"];

ClearAll["Test`*", "Test`*`*"]
TestGui::usage = "TestGui[], testing gui with package";

Begin["`Private`"];

(*misc*)
$guicolor = ColorData[3];
$topcolor = Red;
$topcoloractive = ColorData["GrayTones"][0.4];
$guitextcolor = White;
$guifont = FontFamily -> "Helvetica Neue";
$buttoncolor = $guicolor[5];


TestGui[] := Framed@DynamicModule[
    { view = 1, var1 = 1},
    Column[{
      topRow[Dynamic[view]],
      mainView[Dynamic[{view, var1}]]
      }]
   (* , SaveDefinitions -> True *) 
   (* uncomment this if you are not going to manage this yourself*)
];

mainView[Dynamic[{view_, var_}]] := PaneSelector[
   {1 -> view1[Dynamic[var]],
    2 -> view2[Dynamic[var]]
    },
   Dynamic[view]
   ];

topRow[Dynamic[view_]] := Module[{button1, button2}
, button1 = viewButton[Dynamic[view], "View 1", 1]
; button2 = viewButton[Dynamic[view], "View 2", 2]

; Grid[{{ button1, button2 }}, Spacings -> {0, Automatic} ]

]; (*Module here is only for educational purposes, for such simple element it does not help with anything as one can see in other functions. *)

view1[Dynamic[var_]] := Column[{
    Button["var1 =10", var = 10],
    Dynamic@var
    }];

view2[Dynamic[var_]] := Column[{
    Button["var1 =20", var = 20],
    Dynamic@var,
    "This one has a string here just to justify existance of separate \
view2"
    }];

viewButton[Dynamic[view_], text_, viewnumber_] := Button[
   Style[text, $guitextcolor, $guifont, 
    FontWeight -> Dynamic[If[view == viewnumber, Bold]]], 
   view = viewnumber,
   Background -> 
    Dynamic[If[view == viewnumber, $topcoloractive, $topcolor]], 
   FrameMargins -> Medium, ContentPadding -> True, Method -> "Queued",
    Appearance -> None, ImageSize -> 100
   ];




End[];

EndPackage[];