|
| 1 | +# Layout engine for Windows Forms applications |
| 2 | + |
| 3 | +This is a layout engine for WinForms applications, which took |
| 4 | +inspirations from the following sources: |
| 5 | + |
| 6 | +- [WPF Grid](https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.grid) |
| 7 | +- [ETS Layout](https://www.codeproject.com/articles/116/layout-manager-for-dialogs-formviews-dialogbars-an) |
| 8 | + |
| 9 | +# Example |
| 10 | + |
| 11 | +This is the dialog we'd like to layout: |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +## Requirements |
| 16 | + |
| 17 | +- "pnlLogo" must always be at the left top position |
| 18 | +- "pnlModuleSelector" must fill the space between the logo and the "pnlDashboard" |
| 19 | +- "pnlMenu" and "pnlDialogView" must always stay together |
| 20 | +- "pnlMenu" and "pnlDialogView" must always be in the center of the remaining space |
| 21 | + |
| 22 | +## Solution |
| 23 | + |
| 24 | +We use panes to group together the various controls/panes. |
| 25 | + |
| 26 | +- We always need to create a root pane with a horizontal layout |
| 27 | +- The root pane is split into two sub-panes |
| 28 | + - The pane for the space left to the dashboard |
| 29 | + - The pane for the dashboard |
| 30 | +- The pane for the space left to the dashboard is split into four elements |
| 31 | + - The pane for the "pnlLogo" and the "pnlModuleSelector" (pane 1) |
| 32 | + - The pane to fill the gap between the header and the next pane to keep the next pane centered |
| 33 | + - The pane which contains the panes 4, 5, the "pnlMenu" and the "pnlDialogView" |
| 34 | + - Another pane to fill the gap between the bottom and the previous pane to keep the previous pane centered |
| 35 | + |
| 36 | +## Code for the layout definition |
| 37 | + |
| 38 | +```c# |
| 39 | +_layoutRoot = |
| 40 | + (CreateRoot(this, Orientation.Horizontal) |
| 41 | + << (Pane(Orientation.Vertical).Width(Factor(1)) |
| 42 | + << (Pane().HorizontalStackLayout(VerticalAlignment.Top) |
| 43 | + << Item(pnlLogo) |
| 44 | + << Item(pnlModuleSelector).Width(Factor(1))) |
| 45 | + << Item().Height(Factor(1)) |
| 46 | + << (Pane().HorizontalStackLayout(VerticalAlignment.Center) |
| 47 | + << Item().Width(Factor(1)) |
| 48 | + << Item(pnlMenu) |
| 49 | + << Item(pnlDialogView) |
| 50 | + << Item().Width(Factor(1))) |
| 51 | + << Item().Height(Factor(1))) |
| 52 | + << Item(pnlDashboard)) |
| 53 | + .Build(); |
| 54 | +``` |
| 55 | + |
| 56 | +The functions `CreateRoot`, `Pane`, and `Item` are static functions in the static |
| 57 | + `FubarDev.LayoutEngine.BuilderMethods` class. I'm using a the `using static` C# |
| 58 | + feature to simplify the function calls: |
| 59 | + |
| 60 | + ```c# |
| 61 | + using static FubarDev.LayoutEngine.BuilderMethods; |
| 62 | + using static FubarDev.LayoutEngine.AttachedProperties.AttachedSize; |
| 63 | +``` |
| 64 | + |
| 65 | + This makes all static functions in the `BuilderMethods` class directly available. |
| 66 | + |
| 67 | +### Line-by-line explanation |
| 68 | + |
| 69 | +1. Store the built root pane into the variable `_layoutRoot` |
| 70 | +2. The `CreateRoot` creates the root pane for the `Form` whose elements are aligned horizontally |
| 71 | +3. A sub-pane is created for the space left to the "pnlDashboard" |
| 72 | + - The child elements are stacked vertically |
| 73 | + - The element should consume the whole remaining space (see "*" notation of the WPF grid for grid rows/columns) |
| 74 | +4. A sub-pane is created for the header (pane 1) |
| 75 | + - The child elements are stacked horizontally |
| 76 | + - The default vertical alignment for the elements will be "top" |
| 77 | + - This causes the child elements to keep their height |
| 78 | +5. Add the "pnlLogo" as first child element of the header pane (pane 1) |
| 79 | +6. Add the "pnlModuleSelector" as the second child element which should use the remaining width of the header pane (pane 1) |
| 80 | +7. Adds a spacer pane (pane 2) |
| 81 | +8. Adds the pane containing the spacer panes 4 and 5, and the "pnlMenu" and "pnlDialogView" |
| 82 | + - All child elements should be centered vertically |
| 83 | +9. Adds the spacer pane (pane 4) |
| 84 | + - This pane, together with pane 5 ensures that the "pnlMenu" and "pnlDialogView" are centered horizontally |
| 85 | + - The `.Width(AttachedSize.Factor(1))` on this pane and pane 5 causes the space |
| 86 | + not used by "pnlMenu" and "pnlDialogView" to be split equally between the panes 4 and 5. |
| 87 | +10. Adds the "pnlMenu" |
| 88 | +11. Adds the "pnlDialogView" |
| 89 | +12. Adds the spacer pane (pane 5) |
| 90 | +13. Adds the spacer pane (pane 3) |
| 91 | +14. Adds the "pnlDashboard" |
| 92 | + |
| 93 | +Using `Orientation.Horizontal` for a pane results in the child elements |
| 94 | +being stretched to the full height of the surrounding pane. This behavior |
| 95 | +can be changed by omitting the `Orientation.Horizontal` and using the |
| 96 | +method `.HorizontalStackLayout(VerticalAlignment.Top)` to provide a different |
| 97 | +default vertical alignment. |
| 98 | + |
| 99 | +## Example dump |
| 100 | + |
| 101 | +This dump contains the names of all elements for a layout, together |
| 102 | +with the calculated minimum size for the element. |
| 103 | + |
| 104 | +```text |
| 105 | +root: {X=467,Y=415,Width=1232,Height=988}, {Width=1086, Height=762} |
| 106 | + nonDashboardArea: {X=3,Y=4,Width=875,Height=916}, {Width=753, Height=754} |
| 107 | + headerArea: {X=3,Y=4,Width=875,Height=170}, {Width=154, Height=170} |
| 108 | + pnlLogo: {X=6,Y=8,Width=142,Height=162}, {Width=142, Height=162} |
| 109 | + pnlModuleSelector: {X=154,Y=8,Width=721,Height=162}, {Width=0, Height=162} |
| 110 | + spacerTop: {X=3,Y=174,Width=875,Height=81}, {Width=0, Height=0} |
| 111 | + mainArea: {X=3,Y=255,Width=875,Height=584}, {Width=753, Height=584} |
| 112 | + spacerLeft: {X=3,Y=547,Width=61,Height=0}, {Width=0, Height=0} |
| 113 | + pnlMenu: {X=67,Y=259,Width=225,Height=576}, {Width=225, Height=576} |
| 114 | + pnlDialogView: {X=298,Y=259,Width=516,Height=576}, {Width=516, Height=576} |
| 115 | + spacerRight: {X=817,Y=547,Width=61,Height=0}, {Width=0, Height=0} |
| 116 | + spacerBottom: {X=3,Y=839,Width=875,Height=81}, {Width=0, Height=0} |
| 117 | + pnlDashboard: {X=881,Y=8,Width=321,Height=908}, {Width=321, Height=0} |
| 118 | +``` |
| 119 | + |
| 120 | +## Performing the layout |
| 121 | + |
| 122 | +The layout won't be performed automatically, which is a design decision, |
| 123 | +not a technical decision. |
| 124 | + |
| 125 | +To perform the layout, you must explicitly execute the following code: |
| 126 | + |
| 127 | +```c# |
| 128 | +_layoutRoot.Layout(); |
| 129 | +``` |
| 130 | + |
| 131 | +## Setting the minimum size of a form |
| 132 | + |
| 133 | +The minimum size of the form can only be calculated if the form (and thus all |
| 134 | +its controls) are visible, which is the reason why you should put the following |
| 135 | +code into the `VisibleChanged` event handler of the form: |
| 136 | + |
| 137 | +```c# |
| 138 | +var minSize = _layoutRoot.GetMinimumClientSize(); |
| 139 | +var borderSize = Size - ClientSize; |
| 140 | +MinimumSize = minSize + borderSize; |
| 141 | +``` |
| 142 | + |
| 143 | +# Advanced features |
| 144 | + |
| 145 | +## Overlaps |
| 146 | + |
| 147 | +It's possible to make elements overlap each other. |
| 148 | + |
| 149 | +### Example |
| 150 | + |
| 151 | +```c# |
| 152 | +_layoutRoot = |
| 153 | + (CreateRoot(this, Orientation.Horizontal) |
| 154 | + << (Pane(Orientation.Vertical).Width(Factor(1)) |
| 155 | + << (Pane().HorizontalStackLayout(VerticalAlignment.Top) |
| 156 | + << Item(pnlLogo) |
| 157 | + << Item(pnlModuleSelector).Width(Factor(1))) |
| 158 | + << Pane().Height(Factor(1)).Identifier("a")) |
| 159 | + << Item(pnlDashboard)) |
| 160 | + .AddOverlap( |
| 161 | + "a", |
| 162 | + Pane(Orientation.Vertical) |
| 163 | + << Item().Height(Factor(1)) |
| 164 | + << (Pane().HorizontalStackLayout(VerticalAlignment.Center) |
| 165 | + << Item().Width(Factor(1)) |
| 166 | + << Item(pnlMenu) |
| 167 | + << Item(pnlDialogView) |
| 168 | + << Item().Width(Factor(1))) |
| 169 | + << Item().Height(Factor(1))) |
| 170 | + .Build(); |
| 171 | +``` |
| 172 | + |
| 173 | +As you might've noticed, there's now a new function to `Identifier`. |
| 174 | +This adds an application-global identifier to the given layout element. |
| 175 | +Items that overlap the items with the given `Identifier` may now |
| 176 | +be overlapped with the other controls by calling the `AddOverlap` |
| 177 | +function, which takes the identifier of the layout element to |
| 178 | +be overlapped and the layout that will overlap the target |
| 179 | +layout element. |
| 180 | + |
| 181 | +You may specify more that one "overlap" for a given layout element. |
| 182 | + |
| 183 | +The idea behind this is, that you may have two or more controls |
| 184 | +that should take the same space in the form, but only one should be visible |
| 185 | +at all times. |
0 commit comments