分割窗格 GUI 对象

14

我已经开发了一个gui一段时间了,其中需要创建一些Mathematica缺乏的常见控件对象(例如spinner、treeview、openerbar等)。其中之一是多面板,即将一个面板对象分成两个(或更多)子面板的面板,在这里,鼠标可以设置分隔符。这是我版本的双面板。我想听听你对如何扩展它以处理不仅是2而且任意数量的子面板以及如何优化它的意见和想法。目前,对于负载重的子面板,它明显滞后,不知道为什么。

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, 
           FrameMargins -> -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}]

SplitPane output


(这是一张名为“SplitPane output”的图片)

1
相当令人印象深刻!关于优化:如何使用ControlActive和/或SynchronousUpdating选项? - Sjoerd C. de Vries
非常出色的工作,但请记住,您的“问题”格式不适合该网站。这里的Mma社区通常对此宽容,但请尝试根据网站的一般规则来制定您的问题,以避免关闭(和删除)投票。 - Dr. belisarius
2个回答

14

泛化到多个面板的关键是重构您的代码。目前的形式虽然非常不错,但它将可视化/UI原语和拆分逻辑以及大量重复的代码混合在一起,这使得泛化变得困难。这是重构后的版本:

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]];

下面是可能的外观:

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

enter image description here

我不能评论您提到的性能问题。此外,当您开始用鼠标拖动时,真正的光标位置通常相对于分隔符位置有很大偏差。这适用于您的版本和我的版本,可能需要更精确的缩放。

我只想再次强调 - 只有在我进行重构以将拆分逻辑与与可视化相关的事物分开后,通用化才成为可能。至于优化,我也认为尝试优化这个版本比原始版本更容易,因为原因相同。

编辑

我犹豫了一下是否添加这个注释,但必须提到我的解决方案,虽然可以工作,但显示了一个被专家UI mma程序员认为是不好的做法。即,在Module内部生成变量,并将其用于Dynamic内部(特别是上面代码中的split和各种辅助函数)。我使用它的原因是我无法仅使用DynamicModule生成的变量使其工作,此外Module生成的变量之前总是可以工作的。然而,请参阅John Fultz在 MathGroup线程中的帖子,他指出应该避免使用这种做法。


@Sjoerd 是的。但我对这个话题很感兴趣。另外,我不是一个UI人员,所以有时我会尝试多了解一些相关内容。 - Leonid Shifrin
1
Unevaluated /@ Unevaluated[{content}]: - Dr. belisarius
1
@belisaruis 有什么好笑的?这是一种在列表上映射“Unevaluated”的方法 :)。尽管当前内容被评估,但我编写了大部分代码,以便支持未评估的内容,如果我们希望对“SplitPane”施加一些“Hold”属性(也许这只是一种过度设计)。 - Leonid Shifrin
2
干得好。顺便说一下,鼠标位置与分隔符位置不匹配的原因是因为在 onMouseDraggedCode[n_] 中,temp 是相对于框架左侧(cq 顶部)的分隔符 n 的新位置,但 split[n] 存储了分隔符相对于其直接邻居的位置。这意味着 split[n] 应该设置为类似于 split[n]=temp-Sum[split[i],{i,n-1}] 的东西。 - Heike
@Heike 谢谢,这确实是朝着正确方向迈出的一步 - 我忽略了这一点。但是这只是治标不治本,我在重构时引入了另一个不一致性,而我实际上想要的效果是原始代码和我的解决方案都具有的效果,而这个效果仍然存在。如果你仔细看,当你拖动鼠标时,实际的鼠标位置与分隔符当前所在的位置不同,并且这个差异取决于你在窗格中的位置。我更希望鼠标位置总是固定在分隔符占用的空间上,就像在文本编辑器中通常情况下一样。 - Leonid Shifrin
显示剩余4条评论

6
在Leonid的解决方案上进行了大量改进,这是我的版本。我进行了几个更改,基本上是为了更容易跟踪动态大小更改,并且因为我无法内部化Leonid代码的一部分。
所做的更改:
- 删除了DividerWidth选项,现在用户不能设置它。这不是很重要。 - 最大水平尺寸(上面的maxX)被删除,因为现在它是从用户指定的面板宽度值w计算出来的。 - 第一个参数(w,主要的动态变量)明确保存面板的宽度,而不是保存分隔符位置。此外,它被制作成列表(w[[n]]),而不是函数(如Leonid版本中的split[n])。 - 添加了最小化/恢复按钮到分隔线。 - 限制了分隔线的移动:分隔线只能从其左侧移动到其右侧邻居,不能再进一步移动。 - 微调了分隔线宽度、ImageMargins、FrameMargins、Spacings,以允许大小为零的窗格。
仍需解决的问题:
- 当最小化/最大化分隔线时,它们应该重叠在左/右最边缘。LIFO堆栈可以解决将分隔线设置为最大值,然后尝试通过其按钮更改其他分隔线的问题。这可能会导致一些问题,因为它们返回到以前的状态。堆叠分隔线的问题在Grid中无法解决,或者只能通过非常具体调整的负间距来解决。我认为这不值得处理。 - 当将面板缩小到零宽度/高度时,存在轻微的对齐问题。我可以接受这个问题。
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]];

让我们尝试一下垂直分割窗格:

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"}]

垂直分割示例

这是一个水平分割的窗格:

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"]

水平示例

垂直和水平窗格结合:

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"]

综合示例

我希望听到您对这个解决方案的想法或意见。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接