开发者

Split pane gui object

开发者 https://www.devze.com 2023-04-11 20:52 出处:网络
I\'ve been developing a gui for some time, which requires the creation of common control objects that Mathematica lacks (e.g. spinner, treeview, openerbar, etc.). One is the multipanel, i.e. a pane ob

I've been developing a gui for some time, which requires the creation of common control objects that Mathematica lacks (e.g. spinner, treeview, openerbar, etc.). One is the multipanel, i.e. a pane object that is split into two (or more) subpanes, where the divider can be set by the mouse. Here is my version of a dual pane. I would like to hear your opinion and ideas about how to expand it to handle not just 2 but any number of subpanes, and also how to optimize it. At present, for heavily loaded subpanes, it lags terribly, no idea why.

Options[SplitPane] = {Direction -> "Vertical", 
   DividerWidth -> Automatic, Paneled -> {True, True}};
SplitPane[opts___?OptionQ] := 
  Module[{dummy}, SplitPane[Dynamic[dummy], opts]];
SplitPane[val_, opts___?OptionQ] := SplitPane[val, {"", ""}, opts];
SplitPane[val_, content_, opts___?OptionQ] := 
  SplitPane[val, content, {100, 50}, opts];
SplitPane[Dynamic[split_, arg___], {expr1_, expr2_}, {maxX_, maxY_}, 
   opts___?OptionQ] := 
  DynamicModule[{temp, dir, d, panel, coord, max, fix, val},
   {dir, d, panel} = {Direction, DividerWidth, Paneled} /. {opts} /. 
     Options[SplitPane];
   dir = dir /. {Bottom | Top | "Vertical" -> "Vertical", _ -> 
       "Horizontal"};
   d = d /. Automatic -> 2;
   split = If[NumberQ[split], split, max/2];
   val = Clip[split /. {_?NumberQ -> split, _ -> maxX/2}, {0, maxX}];
   {coord, max, fix} = 
    Switch[dir, "Vertical", {First, maxX, maxY}, 
     "Horizontal", {(max - Last[#]) &, maxY, maxX}];
   panel = (# /. {None | False -> 
          Identity, _ -> (Panel[#, ImageMargins -> 0, 
             FrameMargins -> -1] &)}) & /@ panel;

   Grid[If[dir === "Vertical",
     {{
       Dynamic[
        panel[[1]]@
         Pane[expr1, ImageSize -> {split - d, fix}, 
          ImageSizeAction -> "Scrollable", Scrollbars -> Automatic, 
          AppearanceElements -> None], TrackedSymbols :> {split}],
       Deploy@EventHandler[
         MouseAppearance[
          Pane[Null, ImageSize -> {d*2, fix}, ImageMargins -> -1, 
           FrameMargin开发者_如何转开发s -> -1], "FrameLRResize"],
         "MouseDown" :> (temp = 
            coord@MousePosition@"CellContentsAbsolute"; 
           split = 
            If[Abs[temp - split] <= d \[And] 0 <= temp <= max, temp, 
             split]), 
         "MouseDragged" :> (temp = 
            coord@MousePosition@"CellContentsAbsolute"; 
           split = If[0 <= temp <= max, temp, split])],
       Dynamic@
        panel[[2]]@
         Pane[expr2, ImageSizeAction -> "Scrollable", 
          Scrollbars -> Automatic, AppearanceElements -> None, 
          ImageSize -> {max - split - d, fix}]
       }},
     {
      List@
       Dynamic[panel[[1]]@
         Pane[expr1, ImageSize -> {fix, split - d}, 
          ImageSizeAction -> "Scrollable", Scrollbars -> Automatic, 
          AppearanceElements -> None], TrackedSymbols :> {split}],
      List@Deploy@EventHandler[
         MouseAppearance[
          Pane[Null, ImageSize -> {fix, d*2}, ImageMargins -> -1, 
           FrameMargins -> -1], "FrameTBResize"],
         "MouseDown" :> (temp = 
            coord@MousePosition@"CellContentsAbsolute"; 
           split = 
            If[Abs[temp - split] <= d \[And] 0 <= temp <= max, temp, 
             split]), 
         "MouseDragged" :> (temp = 
            coord@MousePosition@"CellContentsAbsolute"; 
           split = If[0 <= temp <= max, temp, split])],
      List@
       Dynamic[panel[[2]]@
         Pane[expr2, ImageSizeAction -> "Scrollable", 
          Scrollbars -> Automatic, 
          ImageSize -> {fix, max - split - d}, 
          AppearanceElements -> None], TrackedSymbols :> {split}]
      }
     ], Spacings -> {0, -.1}]
   ];
SplitPane[val_, arg___] /; NumberQ[val] := 
  Module[{x = val}, SplitPane[Dynamic[x], arg]];

pos = 300;
SplitPane[
 Dynamic[pos], {Manipulate[
   Plot[Sin[x (1 + a x)], {x, 0, 6}], {a, 0, 2}], 
  Factorial[123]}, {500, 300}]

Split pane gui object


The key to generalizing to several panels was to refactor your code. In its present form, while very nice, it was mixing visualization / UI primitives and options with the split logic, and had lots of duplicate code. This made generalization hard. Here is the refactored version:

ClearAll[SplitPane];
Options[SplitPane] = {
    Direction -> "Vertical", DividerWidth -> Automatic, Paneled -> True
};
SplitPane[opts___?OptionQ] :=   Module[{dummy}, SplitPane[Dynamic[dummy], opts]];
SplitPane[val_, opts___?OptionQ] := SplitPane[val, {"", ""}, opts];
SplitPane[val_, content_, opts___?OptionQ] :=
    SplitPane[val, content, {100, 50}, opts];
SplitPane[sp_List, {cont__}, {maxX_, maxY_}, opts___?OptionQ] /; 
        Length[sp] == Length[Hold[cont]] - 1 :=
  Module[{scrollablePane, dividerPane, onMouseDownCode, onMouseDraggedCode, dynPane,
      gridArg, split, divider, panel},
    With[{paneled = Paneled /. {opts} /. Options[SplitPane],len = Length[Hold[cont]]},
       Which[
          TrueQ[paneled ],
             panel = Table[True, {len}],
          MatchQ[paneled, {Repeated[(True | False), {len}]}],
             panel = paneled,
          True,
            Message[SplitPane::badopt]; Return[$Failed, Module]
       ]
    ];

    DynamicModule[{temp, dir, d, coord, max, fix, val},
      {dir, d} = {Direction, DividerWidth}/.{opts}/.Options[SplitPane];
      dir =  dir /. {
         Bottom | Top | "Vertical" -> "Vertical", _ -> "Horizontal"
      };
      d = d /. Automatic -> 2;
      val = Clip[sp /. {_?NumberQ -> sp, _ -> maxX/2}, {0, maxX}];
      {coord, max, fix} =
        Switch[dir,
          "Vertical",
             {First, maxX, maxY},
          "Horizontal",
             {(max - Last[#]) &, maxY, maxX}
        ];
      Do[split[i] = sp[[i]], {i, 1, Length[sp]}];
      split[Length[sp] + 1] = max - Total[sp] - 2*d*Length[sp];
      panel =
          (# /. {
            None | False -> Identity, 
            _ -> (Panel[#, ImageMargins -> 0,FrameMargins -> -1] &)
           }) & /@ panel;
      scrollablePane[args___] :=
          Pane[args, ImageSizeAction -> "Scrollable", 
               Scrollbars -> Automatic, AppearanceElements -> None];
      dividerPane[size : {_, _}] :=
          Pane[Null, ImageSize -> size, ImageMargins -> -1,FrameMargins -> -1];

      onMouseDownCode[n_] := 
        Module[{old},
          temp = coord@MousePosition@"CellContentsAbsolute";
          If[Abs[temp - split[n]] <= d \[And] 0 <= temp <= max,
            old = split[n];
            split[n] = temp-Sum[split[i], {i, n - 1}];
            split[n + 1] += old - split[n];       
        ]];

      onMouseDraggedCode[n_] :=
         Module[{old},
            temp = coord@MousePosition@"CellContentsAbsolute";
            If[0 <= temp <= max,
               old = split[n];
               split[n] = temp -Sum[split[i], {i, n - 1}];
               split[n + 1] += old - split[n];
            ] ;
         ];

      SetAttributes[dynPane, HoldFirst];
      dynPane[expr_, n_, size_] :=
          panel[[n]]@scrollablePane[expr, ImageSize -> size];

      divider[n_, sizediv_, resizeType_] :=
         Deploy@EventHandler[
            MouseAppearance[dividerPane[sizediv], resizeType],
           "MouseDown" :> onMouseDownCode[n],
           "MouseDragged" :> onMouseDraggedCode[n]
         ];

      SetAttributes[gridArg, HoldAll];
      gridArg[{content__}, sizediv_, resizeType_, sizeF_] :=
         Module[{myHold, len = Length[Hold[content]] },
           SetAttributes[myHold, HoldAll];
           List @@ Map[
             Dynamic,
             Apply[Hold, 
                MapThread[Compose,
                   {
                      Range[len] /. {
                        len :>               
                          Function[
                             exp, 
                             myHold[dynPane[exp, len, sizeF[len]]], 
                             HoldAll
                          ],
                        n_Integer :>
                          Function[exp,
                             myHold[dynPane[exp, n, sizeF[n]],
                                divider[n, sizediv, resizeType]
                             ], 
                          HoldAll]
                      },
                      Unevaluated /@ Unevaluated[{content}]
                    }] (* MapThread *)
               ] /. myHold[x__] :> x
           ] (* Map *)
         ]; (* Module *)
      (* Output *)
      Grid[
        If[dir === "Vertical",
           List@ gridArg[{cont}, {d*2, fix},"FrameLRResize",{split[#] - d, fix} &],
           (* else *)
           List /@ gridArg[{cont}, {fix, d*2},"FrameTBResize", {fix, split[#] - d} &]
        ],
        Spacings -> {0, -.1}]]];

SplitPane[val_, arg___] /; NumberQ[val] := 
   Module[{x = val}, SplitPane[Dynamic[x], arg]];

Here is how it may look:

SplitPane[{300, 300}, 
 {
   Manipulate[Plot[Sin[x (1 + a x)], {x, 0, 6}], {a, 0, 2}], 
   Factorial[123], 
   CompleteGraph[5]
 }, {900, 300}]

Split pane gui object

Can't comment of performance problems you mentioned. Also, when you start dragging with the mouse, the real cursor position is often quite off with respect to the divider position. This is both for your and my versions, perhaps some more precise scaling is needed.

Just want to emphasize once again - generalization became only possible after I did the refactoring, to separate the splitting logic from the visualization-related things. As to the optimization, I also think that it will be much easier to attempt optimizing this version than the original one, for the same reasons.

EDIT

I hesitated a bit to add this note, but it must be mentioned that my solution above, while working, shows one practice which is considered bad by expert UI mma programmers. Namely, it uses Module- generated variables inside Dynamic inner to that Module (in particular, split in the code above, also various auxiliary functions). The reasons I used it are that I wasn't able to make this work with only DynamicModule- generated variables, plus Module- generated variables always worked for me before. However, please see the post by John Fultz in this MathGroup thread, where he states that this practice should be avoided.


Heavily building on Leonid's solution, here is my version. I've applied several changes, basically to make it easier to track dynamic size-changes and because I simply failed to internalize part of Leonid's code.

Changes made:

  • Removed DividerWidth option, now it cannot be set by user. It is not that important.
  • Maximum horizontal size (maxX in the posts above) was dropped as it is now calculated from the user specified panel-width values: w.
  • First argument (w, the main dynamic variable) saves the widths of panels explicitely instead of saving divider positions. Also, it was made to be a list (w[[n]]) instead of a function (as split[n] was in Leonid's version).
  • Added minimize/restore buttons to dividers.
  • Restricted divider movement: dividers can only be moved from their left to their right neigbour, no further movement is possible.
  • Fine tuned divider width, ImageMargins, FrameMargins, Spacings to allow zero-sized panes.

Problems still to deal with:

  • When minimizing/maximizing dividers, they should overlap left/rightmost ones. A LIFO stack of dividers would solve the problem of setting a divider to max, and than trying to change other dividers via their buttons. This might cause some problem, as they set back to previous states. The problem with stacking dividers is that it cannot be solved in Grid, or can only be solved with very specifically tuned negative Spacings. I think it does not worth dealing with.
  • Minor alignment issues when shrinking a panel to zero width/height. This I can live with.

ClearAll[SplitPane];
Options[SplitPane] = {Direction -> "Vertical", Paneled -> True};
SplitPane[opts___?OptionQ] := 
  Module[{dummy = {200, 200}}, SplitPane[Dynamic[dummy], opts]];
SplitPane[val_, opts___?OptionQ] := SplitPane[val, {"", ""}, opts];
SplitPane[val_, content_, opts___?OptionQ] := 
  SplitPane[val, content, Automatic, opts];
SplitPane[Dynamic[w_], cont_, s_, opts___?OptionQ] :=
  DynamicModule[{
    scrollPane, divPane, onMouseDownCode, onMouseDraggedCode, grid,
    dir, panel, bg, coord, mouse, icon, sizeD, sizeB,
    num, old, pos, origo, temp, max, prev, state, fix},

   {dir, panel} = {Direction, Paneled} /. {opts} /. Options@SplitPane;
   dir = dir /. {Bottom | Top | "Vertical" -> "Vertical", _ -> 
       "Horizontal"};
   bg = panel /. {None | False -> GrayLevel@.9, _ -> None};
   panel = 
    panel /. {None | False -> 
       None, _ -> {RGBColor[0.70588, 0.70588, 0.70588]}}; (* 
   Simulate Panel-like colors on the frame. *)
   fix = s /. {Automatic -> If[dir === "Vertical", 300, 800]};

   (* {coordinate function, mouse cursor, button icon, divider size, 
   button size} *)
   {coord, mouse, icon, sizeD, sizeB} = Switch[dir,
     "Vertical", {First, 
      "FrameLRResize", {"\[RightPointer]", "\[LeftPointer]"}, {5, 
       fix}, {5, 60}},
     "Horizontal", {(max - Last@#) &, 
      "FrameTBResize", {"\[DownPointer]", "\[UpPointer]"}, {fix, 
       7}, {60, 7}}
     ];

   SetAttributes[{scrollPane, grid}, HoldAll];
   (* Framed is required below becase otherwise the horizontal \
version of scrollPane cannot be set to zero height. *)
   scrollPane[expr_, size_] := 
    Framed[Pane[expr, Scrollbars -> Automatic, 
      AppearanceElements -> None, ImageSizeAction -> "Scrollable", 
      ImageMargins -> 0, FrameMargins -> 0, ImageSize -> size], 
     FrameStyle -> panel, ImageMargins -> 0, FrameMargins -> 0, 
     ImageSize -> size];
   divPane[n_] :=
    Deploy@EventHandler[MouseAppearance[Framed[
        Item[Button[Dynamic@If[state[[n]], First@icon, Last@icon],

          If[state[[n]], prev[[n]] = w; 
           w[[n]] = max - Sum[w[[i]], {i, n - 1}]; 
           Do[w[[i]] = 0, {i, n + 1, num}]; state[[n]] = False;, 
           w = prev[[n]]; state[[n]] = True;]
          , ContentPadding -> False, ImageSize -> sizeB, 
          FrameMargins -> 0, ImageMargins -> -1, 
          Appearance -> "Palette"], Alignment -> {Center, Center}]
        , ImageSize -> sizeD, FrameStyle -> None, ImageMargins -> 0, 
        FrameMargins -> 0, Background -> bg], mouse], 
      "MouseDown" :> onMouseDownCode@n, 
      "MouseDragged" :> onMouseDraggedCode@n, PassEventsDown -> True];
   onMouseDownCode[n_] := (
     old = {w[[n]], w[[n + 1]]};
     origo = coord@MousePosition@"CellContentsAbsolute";
     );
   onMouseDraggedCode[n_] := (
     temp = coord@MousePosition@"CellContentsAbsolute" - origo;
     w[[n]] = Min[Max[0, First@old + temp], Total@old];
     w[[n + 1]] = Total@old - w[[n]];
     );
   (* Framed is required below because it gives the expression \
margins. Otherwise, 
   if the scrollPane is set with larger than 0 FrameMargins, 
   they cannot be shrinked to zero width. *)
   grid[content_, size_] := 
    Riffle[MapThread[
      Dynamic[scrollPane[Framed[#1, FrameStyle -> None], size@#2], 
        TrackedSymbols :> {w}] &, {content, Range@Length@w}], 
     Dynamic[divPane@#, TrackedSymbols :> {w}] & /@ 
      Range@((Length@w) - 1)];

   Deploy@Grid[If[dir === "Vertical",
      List@grid[cont, {w[[#]], fix} &],
      List /@ grid[cont, {fix, w[[#]]} &]
      ], Spacings -> {0, -.1}, 
     ItemSize -> {{Table[0, {Length@w}]}, {Table[0, {Length@w}]}}],

   Initialization :> (
     (* w = width data list for all panels *)
     (* m = number of panels *)
     (* state = button states *)
     (* prev = previous state of w *)
     (* max = total width of all panels *)
     num = Length@w; state = True & /@ Range@num; 
     prev = w & /@ Range@num; max = Total@w;)
   ];
SplitPane[val_, 
    arg___] /; (Head@val === List \[And] And @@ (NumberQ /@ val)) := 
  Module[{x = val}, SplitPane[Dynamic@x, arg]];

Let's try a vertically splitted pane:

w = {200, 50, 100, 300};
SplitPane[
 Dynamic@w, {Manipulate[Plot[Sin[x (1 + a x)], {x, 0, 6}], {a, 0, 2}],
   Null, CompleteGraph[5], "121234"}]

Split pane gui object

Here is a horizontally splitted pane:

SplitPane[{50, 50, 50, 
  50}, {Manipulate[Plot[Sin[x (1 + a x)], {x, 0, 6}], {a, 0, 2}, 
   ContentSize -> 300], Null, CompleteGraph[5], "121234"}, 
 Direction -> "Horizontal"]

Split pane gui object

Vertical and horizontal panes combined:

xpane = {200, 300};
ypane = {200, 50};
SplitPane[Dynamic@xpane, {
  Manipulate[Plot[Sin[x (1 + a x)], {x, 0, 6}], {a, 0, 2}],
  Dynamic[
   SplitPane[Dynamic@ypane, {CompleteGraph[5], "status"}, Last@xpane, 
    Paneled -> False, Direction -> "Horizontal"], 
   TrackedSymbols :> {xpane}]
  }, 300, Direction -> "Vertical"]

Split pane gui object

I would like to hear your ideas/comments on this solution.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号