# Add controls to a canvas and scale their measures relative to it

I have some code which adds components to a canvas at runtime, WPF application. The user enters some values which translate to the amount of controls to be added and where. There is one canvas which will hold all controls. The controls that get added are rectangles and another canvas.

In my MainWindow class I have the following global fields:

Tile tile = new Tile();
Room room = new Room();
Canvas canvas_room;
List<Tuple<TextBox, Label, string>> values = new List<Tuple<TextBox, Label, string>>();
double canvasHeight;
double canvasWidth;


Then, I set my measurements according to the values from some textboxes the user has to fill in.

 private void setComponents()
{
values.Clear();
values.Add(new Tuple<TextBox, Label, string>(textBox_lengthRoom, label_errRoomLength, (textBox_lengthRoom.Text).Replace(',', '.')));
values.Add(new Tuple<TextBox, Label, string>(textBox_heightRoom, label_errRoomHeight, (textBox_heightRoom.Text).Replace(',', '.')));
values.Add(new Tuple<TextBox, Label, string>(textBox_heightTile, label_errTileLength, (textBox_heightTile.Text).Replace(',', '.')));
values.Add(new Tuple<TextBox, Label, string>(textBox_lengthTile, label_errTileHeight, (textBox_lengthTile.Text).Replace(',', '.')));
values.Add(new Tuple<TextBox, Label, string>(textBox_joints, label_errJoints, (textBox_joints.Text).Replace(',', '.')));

if (checkInput())
{
room.setWidth(Convert.ToDouble(values[0].Item3) * 100);
room.setHeigth(Convert.ToDouble(values[1].Item3) * 100);

tile.setHeigth(Convert.ToDouble(values[2].Item3));
tile.setWidth(Convert.ToDouble(values[3].Item3));
}
}


The checkInput() looks if there are only numeric values in the textboxes. After setting all components, I want rescale them so everything fits nicely in perspective of each other on the canvas. First adding the canvas_room to it, and then adding a rectangle to it for each tile.

private void setComponentsRelativeToEachOther()
{
//*0.98 so canvas borders don't overlap
canvasHeight = canvas_main.ActualHeight * 0.98;
canvasWidth = canvas_main.ActualWidth * 0.98;
double ratio;
setComponents();

if (room.getHeigth() > room.getWidth())
{
ratio = canvasHeight / room.getHeigth();

tile.setWidth(tile.getWidth() * ratio);
tile.setHeigth(tile.getHeigth() * ratio);

room.setWidth(room.getWidth() * ratio);
room.setHeigth(room.getHeigth() * ratio);
}
else if (room.getHeigth() < room.getWidth())
{
ratio = canvasWidth / room.getWidth();

tile.setWidth(tile.getWidth() * ratio);
tile.setHeigth(tile.getHeigth() * ratio);

room.setWidth(room.getWidth() * ratio);
room.setHeigth(room.getHeigth() * ratio);
}
else if (room.getHeigth() == room.getWidth())
{
if (canvasHeight > canvasWidth)
{
ratio = canvasWidth / room.getWidth();

tile.setWidth(tile.getWidth() * ratio);
tile.setHeigth(tile.getHeigth() * ratio);

room.setWidth(room.getWidth() * ratio);
room.setHeigth(room.getHeigth() * ratio);
}
else
{
ratio = canvasHeight / room.getHeigth();

tile.setWidth(tile.getWidth() * ratio);
tile.setHeigth(tile.getHeigth() * ratio);

room.setWidth(room.getWidth() * ratio);
room.setHeigth(room.getHeigth() * ratio);
}
}
}


So I define a ratio with which all measures will be multiplied. Based on the longest edge of the room (room will hold the measures and these will be given to canvas_room when the room gets placed). place room:

private void placeRoom()
{
canvas_room = new Canvas();
canvas_room.Width = room.getWidth();
canvas_room.Height = room.getHeigth();
canvas_room.ClipToBounds = true;
canvas_room.Margin = new Thickness(10);

canvas_main.Children.Add(canvas_room);
Canvas.SetLeft(canvas_room, 0);
Canvas.SetTop(canvas_room, 0);

Border border_room = new Border();
border_room.BorderBrush = Brushes.Red;
border_room.BorderThickness = new Thickness(2);
border_room.Height = room.getHeigth();
border_room.Width = room.getWidth();

canvas_room.Children.Add(border_room);
}


place tile:

private void placeTile(double x, double y)
{
Rectangle rect = new Rectangle();
rect.StrokeThickness = 1;
rect.Width = tile.getWidth();
rect.Height = tile.getHeigth();
rect.Stroke = Brushes.Black;

canvas_room.Children.Add(rect);
Canvas.SetLeft(rect, x);
Canvas.SetTop(rect, y);
}


I would like to know if this is the most efficient way of doing this. What are my mistakes in the way of writing code. I am still pretty new to coding and would like to get better in making decisions when it comes to logic and declaring fields. Is it ok to put all my functions in 1 class? For the room and tile object I have different classes but they only hold the measurements as properties and a constructor, nothing else.

• generally if you need relative positioning then you shouldn't be using a canvas – MikeT Apr 10 '17 at 16:46
• @MikeT Why not ? Can I place elements on the window itself then, or how would i do it then? – Erik Apr 11 '17 at 6:29
• because positioning items with fixed coordinates presupposes fixed locations, if you need relative spacing i would suggest grids, another options would be to leave the positions fixed and then apply a transformation to a viewbox, this would allow you to scale your view to fit the area – MikeT Apr 11 '17 at 9:27

## 1 Answer

A canvas works on absolute positions so shouldn't be used for any relative layouts

However there are several options available in the WPF framework

the one that I think fits your needs most is transformation

once you have your fixed not relative canvas you can apply a transformation to the entire canvas and scale the entire thing, the easiest way to do this is to use a viewbox as this automatically scales its content to fit its available area while maintaining the aspect ratio

another option is if you were trying to do a simple grid layout (eg battleship style) then you can just use a grid and this will fit the available space though with out regard to the aspect ratio

Here is an Example of these 2 alternative layouts

<Window x:Class="WpDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpDemo"
mc:Ignorable="d"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.Column="1" Grid.Row="3" Grid.ColumnSpan="3" Grid.RowSpan="2" Fill="Aqua"/>
<Rectangle Grid.Column="6" Grid.Row="3" Grid.ColumnSpan="3" Grid.RowSpan="4" Fill="Black"/>
<Rectangle Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="1" Grid.RowSpan="2" Fill="Green"/>
<Rectangle Grid.Column="1" Grid.Row="7" Grid.ColumnSpan="1" Grid.RowSpan="1" Fill="Red"/>
<Rectangle Grid.Column="2" Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="1" Fill="Yellow"/>
</Grid>
<Viewbox >
<Canvas Width="200" Height="200">
<Rectangle Canvas.Top="10" Canvas.Left="30" Width="60" Height="30" Fill="Aqua" />
<Rectangle Canvas.Top="60" Canvas.Left="30" Width="60" Height="100" Fill="Black"/>
<Rectangle Canvas.Top="0" Canvas.Left="10" Width="20" Height="20" Fill="Green"/>
<Rectangle Canvas.Top="10" Canvas.Left="70" Width="80" Height="20" Fill="Red"/>
<Rectangle Canvas.Top="80" Canvas.Left="10" Width="30" Height="16" Fill="Yellow"/>
</Canvas>
</Viewbox>
</Grid>
</Window>

• I see what you're saying. But I do not know the measurements in advance, only when the program is already running and the user fills in some fields and presses a button. You're saying I should add a canvas to a viewbox at runtime based on the measurements the user fills in (even if the viewbox will exceed the size of the window?) and then fill the viewbox with rectangles, and then transform the viewbox to be scaled to the window size? or am I misunderstanding? – Erik Apr 11 '17 at 10:59
• if you are using a ViewModel have your canvas's Height and width bound to the VM you can then calculate this at runtime from the collection changed event of your collection, the viewbox will fill its designated space completely, it will then scales what ever is inside it to fit in that space – MikeT Apr 11 '17 at 11:12
• Thanks a lot, this helps remove much of the calculations which made the whole thing really messy. – Erik Apr 11 '17 at 11:50