Dev Logging

printf("does this work\n");
in

November 2008 - Posts

Building a Column-Major UniformGrid in WPF

A colleague of mine posed an interesting problem to me the other day. He was building a prototype in WPF that involved two columns of equally sized items. The UniformGrid panel seemed a natural fit, but there was a small problem: the UniformGrid adds everything in row-major order, whereas he required items to be displayed in column-major order. Setting aside more practical or robust suggestions (a Grid with attached row and column properties set programmatically or a custom panel, respectively), he was looking for a quick and dirty hack to get the desired result. Here's what he had (left) and what he was looking for (right):

What he had: a UniformGrid in row-major order. The desired result: the same UniformGrid in column-major order.

After considering the problem briefly, I decided the best strategy would be to employ the seldom-used LayoutTransform. For doing transformations in WPF, RenderTransforms are usually the default choice, and with good reason, but it's important to understand the distinction. Transforms are usually used to tweak the size, location, and orientation of elements on the screen; in such situations, affecting the layouts of other elements is rarely the desired outcome. RenderTransforms are a good fit for this scenario, because they take effect after layout and just before rendering. Here's an example of RenderTransforms in action:

RenderTransforms in action! 

   1:  <StackPanel>
   2:      <Button Background="Red" Content="2" Foreground="White"/>
   3:      <Button Background="Green" Content="5" Foreground="White" RenderTransformOrigin="0.5 0.5">
   4:          <Button.RenderTransform>
   5:              <RotateTransform Angle="45"/>
   6:          </Button.RenderTransform>
   7:      </Button>
   8:      <Button Background="Blue" Content="6" Foreground="White"/>
   9:  </StackPanel>

Notice how the middle element can rotate freely without affecting the layout of the screen or paying heed to the restrictions of the space provided for it. In contrast, LayoutTransforms take effect just before layout:

LayoutTransforms in action! 

In this case, the transformation has an effect on the layout of the screen and on the button's size. If we had rotated to a full 90 degrees . . .

The button expands to fill the space given to it during layout.

   1:  <StackPanel>
   2:      <Button Background="Red" Content="2" Foreground="White"/>
   3:      <Button Background="Green" Content="5" Foreground="White">
   4:          <Button.LayoutTransform>
   5:              <RotateTransform Angle="45"/>
   6:          </Button.LayoutTransform>
   7:      </Button>
   8:      <Button Background="Blue" Content="6" Foreground="White"/>
   9:  </StackPanel>

The button would expand to fill the space given to it. This may not be totally intuitive, but it's important to our original problem. If you recall, we were thinking of ways to hack a UniformGrid into adding objects in column-major order. So what happens if we simply apply a 90 degree LayoutTransform to our UniformGrid?

The UniformGrid rotated 90 degrees.

Hmm . . . not quite what we were hoping for, but promising. In particular, it's nice to see the elements resize themselves to take advantage of the space provided automatically. But there are two problems to note here. The obvious one is that the contents of the buttons are sideways. The more subtle flaw in our solution is that it's backwards: the first column is on the right instead of on the left. We'll tackle the latter first by adding flipping the UniformGrid via a ScaleTransform:

The same panel reversed in the X direction.

   1:  <UniformGrid Rows="2" Columns="4">
   2:      <UniformGrid.LayoutTransform>
   3:          <TransformGroup>
   4:              <RotateTransform Angle="90"/>
   5:              <ScaleTransform ScaleX="-1"/>
   6:          </TransformGroup>
   7:      </UniformGrid.LayoutTransform>
   8:      <Button Background="White" Content="1"/>
   9:      <Button Background="Red" Content="2" Foreground="White"/>
  10:      <Button Background="Orange" Content="3"/>
  11:      <Button Background="Yellow" Content="4"/>
  12:      <Button Background="Green" Content="5" Foreground="White"/>
  13:      <Button Background="Blue" Content="6" Foreground="White"/>
  14:      <Button Background="Violet" Content="7"/>
  15:      <Button Background="Black" Content="8" Foreground="White"/>
  16:  </UniformGrid>

Ouch. Two steps forward, one step back. Now the numbers are not just sideways, but also backwards! We can solve both problems by applying transformations to the buttons reversing our earlier manipulations:

The buttons are corrected by applying opposite transformations.

   1:  <Style TargetType="Button">
   2:      <Setter Property="LayoutTransform">
   3:          <Setter.Value>
   4:              <TransformGroup>
   5:                  <RotateTransform Angle="-90"/>
   6:                  <ScaleTransform ScaleY="-1"/>
   7:              </TransformGroup>
   8:          </Setter.Value>
   9:      </Setter>
  10:  </Style>

Note here that we've scaled the buttons in the Y direction, whereas the panel is scaled in the X direction. As a result of the transforms applied to the panel, the Y axis from the elements' perspective goes from left to right.

Of course, as we noted before we began, this represents a quick, hacked together solution. Happily, this was enough for my friend's prototype. If you have any questions or better solutions, be sure to leave a comment.