6
$\begingroup$

I want to know how to write clean code of a big DynamicModul for a complex GUI which should be deployed as a CDF. I want to split the dynamic module into several smaller DynamicModuls and Functions.

Here is an example package:

(* ::Package:: *)
ClearAll["test`*", "test`*`*"]


BeginPackage["test`"];

testgui::usage="testing gui with package";

Begin["`Private`"];

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

view1 = DynamicModule[{var1 = 1}, Column[{Button["var1 =10", var1 = 10], Dynamic@var1}], InheritScope -> True];
view2 = DynamicModule[{var1 = 1}, Column[{Button["var1 =20", var1 = 20], Dynamic@var1}], InheritScope -> True];

testgui := Framed@DynamicModule[{stream, viewbutton, view = 1, toprow, mainview, var1 = 1},
  Column[{Dynamic@toprow,
    Dynamic@mainview[view]
  }],

  Initialization :> (
    mainview = <|1 -> view1, 2 -> view2|>;

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

    toprow = Grid[{{Dynamic@viewbutton["View 1", 1], Dynamic@viewbutton["View 2", 2]}}, Spacings -> {0, Automatic}];
  )
]


End[];

EndPackage[ ];

I saved this package code as "test.wl" and use Get to test it:

packagepath = NotebookDirectory[] <> "test.wl"
Get[packagepath]
testgui

The GUI looks like this and works as expected:

The GUI

My specific questions are:

1.) How can I move the function viewbutton out from Initialization?

Simply copying the function outside of Initialization does not work. Also when it is wrapped inside another DynamicModule with InheritScope -> True.

2.) In general, what is the best way to organise projects with complex custom GUIs inside a package? With the aim to have small individual parts which can be tested and developed individually.

I am thinking about making view1 and view2 also modular and containing several smaller DynamicModules and use functions similar to the viewbutton function inside view1 and view2.

$\endgroup$
  • $\begingroup$ slightly related: How can I include functions from a package into a CDF file? $\endgroup$ – Kuba Jan 19 '18 at 13:02
  • 1
    $\begingroup$ No, that works for "normal" functions but not for functions which are "dynamic" itself. It does not work with the viewbutton function, or at least did not get it to work. $\endgroup$ – gogoolplex Jan 19 '18 at 13:28
  • $\begingroup$ Ah, ok, I see. Well, the standard way is move viewbutton and define it as viewbutton[text_, viewnumber_, Dynamic[view_], ...] $\endgroup$ – Kuba Jan 19 '18 at 13:32
  • $\begingroup$ Ah ok thanks, this led me on the right path. I am almost there, and will continue working on it on Tuesday. Is Dynamic as a function argument documented somewhere? $\endgroup$ – gogoolplex Jan 19 '18 at 16:06
  • $\begingroup$ @gogoolplex almost anything can be a function argument. Do some searching on the site for defining DownValues. The only real thing to watch out is HoldPattern. $\endgroup$ – b3m2a1 Jan 19 '18 at 22:13
3
+50
$\begingroup$

There are few aspects:

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[];
$\endgroup$
  • 1
    $\begingroup$ Here's an example of a pretty complex interface system built on variable passing and modularization (I'm sure you have a collection of your own you could show too). It doesn't protect against restarts, though. There's also this for making a little card game. Just thought it might be nice to link to some more examples. $\endgroup$ – b3m2a1 Mar 16 '18 at 22:03
  • $\begingroup$ Thanks for your answer. This is pretty much what I used after your comment above. The only thing which I found useful and were not obvious for me was to also use stuff like topRow[Dynamic[view_]] := Module[...] or topRow[Dynamic[view_]] := DynamicModule[...] depending on the application. In addition its good to know that for example topRow[Dynamic[view_Integer]] does not work but topRow[Dynamic[view_?IntegerQ]] does. $\endgroup$ – gogoolplex Mar 17 '18 at 21:01
  • $\begingroup$ @gogoolplex I added a note about Dynamic and a Module example. Yes ?IntegerQ forces evaluation to check condition which is why it works even for held Dynamic content. $\endgroup$ – Kuba Mar 18 '18 at 20:30
  • $\begingroup$ Thank for the answer. I have come to expect that we should use Dynamic for displaying things, except for the first argument of controls. So I was confused about this usage in f[Dynamic[arg_]]:=definition. I now realize that this Dynamic is there just because of its HoldFirst attribute. In fact you can change most of the Dynamics above with Holds, and it works just as well. I prefer to use Hold. Dynamic is already quite complex as it is. $\endgroup$ – Gustavo Delfino Mar 19 '18 at 18:03
  • $\begingroup$ @GustavoDelfino yes, sorry I didn't put this comment from the beginning. Let me know if enything else is not clear or is there something that should but wasn't addressed. p.s. I like to use Dynamic when it comes to passing DynamicModule or gui related variables and to use HoldAll/Hold approach for purely kernel functions. $\endgroup$ – Kuba Mar 19 '18 at 21:49
2
$\begingroup$

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.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.