-
Notifications
You must be signed in to change notification settings - Fork 1
Home
The provided layout system allows any kind of objects that can be given a position and a size to be easily laid out in an intuitive manner. It just needs to be told how those objects should be aligned and sized, relative to each other and to the available space, and what the minimum distances between them should be. These relationships are then maintained, no matter how certain aspects of those objects (e.g. text size) are changed or how the window is resized.
This layout system makes use of two basic classes: the Widget
class and the Sizer
class.
The Widget class is a wrapper class for widget objects of an existing GUI system. It is meant to be modified; specifically, its set_pos
and set_size
methods should be adapted to apply the correct position and size to the actual widget object. Other code that deals directly with the object's position and size will also need to be adjusted.
In short, the Widget class is the "glue" between the existing GUI system and the layout system.
The Sizer class is responsible for computing the layout. All objects that are part of the layout need to be added to a sizer.
When the gui package is imported and the GUI class is instantiated, one sizer is already created and available:
from gui import *
...
gui = GUI(gui_framework)
main_sizer = gui.sizer
Sometimes it can be useful to have a sizer with a minimum size, regardless of its contents. For this purpose, a sizer can be given a "default size", i.e. its minimum size without containing any objects:
sizer.default_size = (width, height)
When a layout is complete or edited, or the minimum size of a widget has changed due to a property change, GUI.layout
needs to be called to update the layout:
from gui import *
...
gui = GUI(gui_framework)
main_sizer = gui.sizer
...
# Build the layout
...
gui.layout()
Three types of objects can be added to a sizer:
- Widget class instances;
-
(width, height)
size tuples (to add empty space); - other (sub-)sizers.
When objects are added, the sizer "grows" in a certain direction: either horizontally (left to right) or vertically (top to bottom). The given gui.sizer
is a vertically growing sizer.
The grow direction is passed into the Sizer constructor as first argument: "horizontal"
or "vertical"
.
# create a horizontally growing sizer
sizer = Sizer("horizontal")
# wrap an existing GUI object in a Widget instance, so it can be added to
# a sizer
widget = Widget(gui_obj)
# create a vertical sub-sizer
subsizer = Sizer("vertical")
# add the widget to the horizontal sizer...
sizer.add(widget)
# ...followed by some empty space...
sizer.add((20, 0))
# ...to separate it from the sub-sizer
sizer.add(subsizer)
Objects can also be inserted into a sizer, by specifying an integer for the index
parameter of the add
method (the default value is None
, to indicate that the object should simply be appended):
# insert a widget before the third object in the sizer
sizer.add(widget, index=2)
When an object is added to a sizer, a "cell" is created to hold it. This is an instance of a special SizerCell
class, which is dedicated to managing certain sizer-related properties associated with the object. This class also serves as an abstraction of the three types of objects that can be added to a sizer, so they can be handled in the same way.
A Widget or Sizer instance keeps a reference to the SizerCell that it's inside of.
Although objects can't be retrieved from a sizer directly, their cells can. An object itself can then be retrieved from its cell.
# retrieve the third cell in the sizer
sizer_cell = sizer.cells[2]
# do something with the object in the cell, but only if it's a widget
# (and not a size or sub-sizer)
if sizer_cell.type == "widget":
widget = sizer_cell.object
...
The add
method itself also returns the cell that it creates:
# keep a reference to the cell created for the object when adding it to the sizer
cell = sizer.add(widget)
Some widgets might have child widgets. To have these controlled by the layout system as well, the sizer they need to be added to has to be assigned to the parent widget, instead of being made a sub-sizer of another sizer:
parent_widget = Widget(parent_obj)
children_sizer = Sizer("vertical")
parent_widget.sizer = children_sizer
A cell can be removed from a sizer, with the option to destroy it and whatever object it holds:
sizer_cell = widget.sizer_cell
sizer.remove_cell(sizer_cell, destroy=True)
If the cell is not destroyed, it can be re-added to the same sizer or to a different sizer afterwards:
sizer_cell = widget.sizer_cell
sizer1.remove_cell(sizer_cell)
# insert the cell into another sizer, before its second cell
sizer2.add_cell(sizer_cell, index=1)
By default, objects will be added to one single row (in the case of a horizontal sizer) or column (for a vertical sizer), no matter how many are added.
It is possible to change this behavior by assigning a value bigger than zero to the prim_limit
parameter of the Sizer constructor. Whenever an object would be added to the current row (or column) beyond the limit imposed by this value, that object will instead be added to a new row (or column). This will result in a grid-like layout, with rows as well as columns. In this context, the "grow direction" of the sizer is its "primary direction" (hence the name prim_dir
of the first constructor parameter), while the other direction is then considered to be its "secondary direction".
# objects will be added horizontally, with a maximum of 3 objects per row
sizer1 = Sizer("horizontal", prim_limit=3)
# objects will be added vertically, with a maximum of 4 objects per column
sizer2 = Sizer("vertical", prim_limit=4)
All cells that belong to the same row will have the same height, while all those in one and the same column will have the same width. The minimum height of a row is the largest of the minimum heights of all of its cells. Similarly, the minimum width of a column is the largest of the minimum widths of all of its cells.
To keep a specific, minimum distance between sizer cells, it is possible to define space between the rows and columns of that sizer.
These horizontal and vertical "gaps" can be set separately with the gaps
parameter of the Sizer constructor:
# create a sizer with 20-pixel gaps between columns and 10-pixel gaps between rows
sizer = Sizer("horizontal", prim_limit=3, gaps=(20, 10))
Borders can be defined for an object when it is added to a sizer. These are the left, right, bottom and top offsets from the corresponding sides of its cell.
A tuple consisting of the four corresponding integer values is passed in to the add
method for the borders
parameter:
offset_left = 10
offset_right = 5
offset_bottom = 20
offset_top = 15
borders = (offset_left, offset_right, offset_bottom, offset_top)
sizer.add(widget, borders=borders)
The minimum size of a cell is the sum of the minimum size of the contained object and the borders around that object.
The available space in a sizer depends on the minimum sizes of its rows and columns and the gaps between them. When there is more than strictly needed by a row or column, a choice needs to be made regarding the use of this space.
A proportion is used to decide how much of the available space (in a certain direction) will be taken up by a particular row or column (and transferred to all of its cells).
Default proportions can be set per sizer (local defaults), but also for all sizers (global defaults). The initial local default proportion for rows and columns is -1.0
, meaning that the global default proportions (initially set to 0.0
) will be used instead.
# set a default vertical proportion of 1.0 for all rows of all sizers but no
# new default horizontal proportion for any columns
Sizer.set_global_default_proportions(row_proportion=1.)
# create a new sizer...
sizer = Sizer("horizontal")
# ...and set its own default vertical proportion for all of its rows to 0.0
# to override the global default value, but leave the default horizontal
# value to -1.0, so the global default will be used for that instead
sizer.set_default_proportions(row_proportion=0.)
To request specific proportions for a particular object added to a sizer, pass in the desired values to the Sizer.add
method. Both horizontal and vertical proportions need to be specified together for the proportions
parameter:
# Add a widget, requesting a horizontal proportion of 1.0 for its column but no
# individual vertical proportion for its row
sizer.add(widget, proportions=(1., 0.))
The requested proportions get associated with the cell of the added object. That means that when the cell is removed from its sizer and later added to a different row and/or column, those proportions will then affect that new row and/or column instead of the old one. This can preserve the way the object gets resized.
If no cells of a particular row or column have a non-zero proportion, this row or column will not make use of the excess space at all; it will simply keep its minimum size.
A proportion is only meaningful in the presence of other rows or columns with a proportion. If only one row or column has a non-zero proportion, it will take up ALL available space, no matter how small the given value actually is. When there are multiple rows or columns with non-zero proportions, the available space is divided between them, depending on the ratio of those proportions:
sizer = Sizer("horizontal")
# request for column of widget1 to take up 1/3 of the available horizontal space
sizer.add(widget1, proportions=(1., 0.))
# request for column of widget2 to take up 2/3 of the available horizontal space
sizer.add(widget2, proportions=(2., 0.))
In the above example, widget1
's column gets one third of the available space, while widget2
's column gets two thirds.
The actual values don't matter that much; they could be 2.0 and 4.0 or 0.5 and 1.0 instead of 1.0 and 2.0 and the result would be the same.
Especially when working with a grid-like sizer, it is important to note that the proportion applied to a column will be the largest of all horizontal proportions associated with its cells. Similarly, the proportion applied to a row will be the largest of all vertical proportions associated with its cells.
Sometimes it may be convenient to have a row or column set to resize proportionately (overriding the defaults), without having to associate that proportion with any of its cells (e.g. when the index of that row or column is known in advance). To that end, an explicit proportion can be set for a row or column. In that case, it overrides the corresponding proportions that have been associated with the cells of that row or column:
# create a horizontal sizer with 2 columns
sizer = Sizer("horizontal", prim_limit=2)
# set an explicit horizontal proportion for the second column
sizer.set_column_proportion(index=1, proportion=1.)
# add widget1 to the first column, requesting a horizontal proportion of 1.5
sizer.add(widget1, proportions=(1.5, 0.))
# add widget2 to the second column, requesting a horizontal proportion of 3.0;
# this will have no effect, since an explicit proportion of 1.0 has been set
# for the second column
sizer.add(widget2, proportions=(3., 0.))
# add widget3 to the first column, requesting a horizontal proportion of 2.0;
# the proportion to be used for the first column is now set to 2.0, since this
# is a bigger value than the proportion of 1.5 which was previously requested
sizer.add(widget3, proportions=(2., 0.))
# add widget4 to the second column, without requesting a horizontal proportion;
# the cell that contains widget4 will nevertheless still take up all of the width
# assigned to the second column, just like widget2's cell
sizer.add(widget4)
# add widget5 to the first column, requesting a horizontal proportion of 0.5;
# this will not work, because this is a smaller value than the proportion of 2.0
# which was previously requested
sizer.add(widget5, proportions=(.5, 0.))
It is also possible to clear an explicitly set proportion, in which case the largest of the corresponding proportions associated with the cells of the row or column is used instead (or the defaults, if no cells have proportions associated with them):
# clear the explicit horizontal proportion that was set for the third column
sizer.clear_column_proportion(index=2)
# clear the explicit vertical proportions that were set for any rows
sizer.clear_row_proportions()
# clear the explicit proportions that were set for any rows or columns
sizer.clear_proportions()
Although borders are never scaled, they are considered to be an integral part of the cell, which is important to keep in mind when setting proportions. Consider the following example:
sizer = Sizer("horizontal")
sizer.add(widget1, proportions=(1., 0.))
sizer.add(widget2, proportions=(1., 0.), borders=(20, 20, 0, 0))
sizer.add(widget3, proportions=(1., 0.))
The code above will produce a layout in which the middle widget looks smaller than the other two, due to the borders not being taken into account when distributing the available width to their columns. If this is a problem, it might be preferable to insert size tuples in between the widgets, even though these additional objects will make computing the layout less efficient. But in case these borders are all equal and should appear between all widgets, inserting gaps between the columns would be an even better solution.
If the size of a cell is bigger than its minimum size, the object within can occupy the cell space in several ways. It can be set to expand itself to take up the entire width and/or height of the cell, it can be centered horizontally and/or vertically within the cell, or it can be left-, right-, bottom- or top-aligned.
So the available values (for each direction) for the alignments
parameter are:
-
"expand"
(the default for both directions) -
"min"
(left-aligned in horizontal direction, top-aligned in vertical direction) -
"max"
(right-aligned in horizontal direction, bottom-aligned in vertical direction) "center"
Just like proportions, both horizontal and vertical alignments need to be specified together:
sizer = Sizer("horizontal")
# request for widget1 to be left-aligned while taking up all available
# vertical space within its cell
sizer.add(widget1, alignments=("min", "expand"))
# request for widget2 to be centered horizontally while being bottom-aligned
# within its cell
sizer.add(widget2, alignments=("center", "max"))
Assigning space to objects in a sizer occurs in several steps.
- In the first step, a certain width and height get assigned to the sizer itself.
- In a second step, the width is assigned proportionately to the columns of the sizer, while the height is assigned proportionately to its rows. Each cell (the "intersection" of a particular row and column) will have the width of its column and the height of its row. These proportions can be set directly for a specific row or column, or they can be associated with a specific cell. The former will override the latter. In case only the latter is done, a row or column will be given the largest of the proportions associated with its cells. If neither is done, default values will be applied.
- In a third and final step, an object within a cell can be assigned all of its cell's width and/or height by setting its horizontal and/or vertical alignment to
"expand"
. (The other alignment options leave the object at its minimum size and only affect its placement within its cell.)