#1,062 – Scaling a Canvas Using a ViewBox

ViewBox is typically used to scale a panel containing other elements.  One common use of a ViewBox is to scale the contents of a Canvas panel.

We might include several elements within a Canvas that has an explicit size.

1062-001

If we re-size the window, however, the canvas stays the same size.

1062-002

We could have had the Canvas stretch to fill the remaining area, but its elements would still be the same size.

We can get the elements within the Canvas to scale by wrapping the Canvas in a ViewBox.

    <DockPanel>
        <Label DockPanel.Dock="Top" Background="LightGray"
               Content="Stuff at top of window here"
               VerticalAlignment="Top"/>
        <Label DockPanel.Dock="Bottom" Background="AliceBlue"
               Content="Bottom stuff down here"
               VerticalAlignment="Bottom"/>
        <Viewbox>
            <Canvas Background="Bisque" Width="200" Height="100">
                <Line X1="5" Y1="5" X2="195" Y2="95"
                        Stroke="Black"/>
                <Label Canvas.Left="80" Canvas.Top="5" Content="Howdy"/>
                <Ellipse Height="30" Width="50" Stroke="Blue" StrokeThickness="2"
                            Canvas.Left="140" Canvas.Top="5"/>
            </Canvas>
        </Viewbox>
    </DockPanel>

Now when we resize the window, everything within the Canvas is scaled.
1062-003

 

#1,057 – Preventing a Grid from Clipping a Child Element

Grid will normally clip child elements within each grid cell so that the element will not extend beyond the bounds of the cell.  In the example below, the Label in the second column is clipped to the right side of the column.

    <Grid Background="AliceBlue" Margin="25" ShowGridLines="True">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Label Grid.Column="1"
                Content="Each man delights in the work that suits him best." Background="Olive"
                VerticalAlignment="Center"
                Margin="10"/>
    </Grid>

1057-001
If we want to prevent the Grid from clipping the element, we can do this by placing the element in a Canvas, which we then put into the Grid.  Because a Canvas does not clip child elements,  this will allow the element to extend beyond the boundaries of the Grid.

    <Grid Background="AliceBlue" Margin="25" ShowGridLines="True">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Canvas Grid.Column="1">
            <Label Content="Each man delights in the work that suits him best." Background="Olive"
                   VerticalAlignment="Center"
                   Margin="10"/>
        </Canvas>
    </Grid>

1057-002

#1,055 – Canvas Does Not Clip Child Elements

By default, the Canvas panel does not clip its child elements at the boundaries of the Canvas.  If the child element does not fit entirely within the Canvas, it will extend beyond the edge of the Canvas.

For example, assume that we put a Label on a Canvas:

    <Canvas Background="AliceBlue" Margin="25">
        <Label Content="Each man delights in the work that suits him best." Background="Olive"
               Margin="10"/>
    </Canvas>

If we place the Canvas in a large enough Window, the Canvas will be large enough to contain the Label.

1055-001

If we now make the window smaller, the Canvas will become smaller and the Label will extend beyond the boundaries of the Canvas.  Note, however that the Label will not extend past the edge of the Window.

1055-002

1055-003

#999 – Using a Canvas as the Items Panel for a ListBox

You can replace the default StackPanel used as the items panel for a ListBox with any other panel element.  If you have items that you want to display at arbitrary locations, you can use a Canvas for your items panel.

The example below presents a list of cities, where each city is placed at its proper latitude and longitude.

Assuming that we have a City class that accepts a name and latitude/longitude values passed to its constructor, we can create a list of cities:

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;

            CityList = new ObservableCollection<City>
            {
                new City("Duluth", 46.83, 92.18),
                new City("Redmond", 44.27, 121.15),
                new City("Tucson", 32.12, 110.93),
                new City("Denver", 39.75, 104.87),
                new City("Boston", 42.37, 71.03),
                new City("Tampa", 27.97, 82.53)
            };
        }

        private ObservableCollection<City> cityList;
        public ObservableCollection<City> CityList
        {
            get { return cityList; }
            set
            {
                cityList = value;
                RaisePropertyChanged("CityList");
            }
        }

        // INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        private void RaisePropertyChanged(string propName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

We then bind a ListBox to this list of cities.  We also:

  • Use its ItemContainerStyle to map latitude and longitude values to the attached Top and Left properties of the Canvas element
  • Use value converters to convert latitude and longitude values to canvas positions
  • Specify the Canvas as the ItemsPanel
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow"
        Width="470" Height="310">

    <Window.Resources>
        <ResourceDictionary>
            <local:LatValueConverter x:Key="latValueConverter" />
            <local:LongValueConverter x:Key="longValueConverter" />
            <sys:Double x:Key="mapWidth">440</sys:Double>
            <sys:Double x:Key="mapHeight">240</sys:Double>
        </ResourceDictionary>
    </Window.Resources>

    <StackPanel Orientation="Horizontal" Margin="5" >
        <ListBox ItemsSource="{Binding CityList}"
                 DisplayMemberPath="Name">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Canvas.Left"
                            Value="{Binding Longitude, Converter={StaticResource longValueConverter},
                                            ConverterParameter={StaticResource mapWidth}}"/>
                    <Setter Property="Canvas.Top"
                            Value="{Binding Latitude, Converter={StaticResource latValueConverter},
                                            ConverterParameter={StaticResource mapHeight}}"/>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas IsItemsHost="True"
                            Width="{StaticResource mapWidth}"
                            Height="{StaticResource mapHeight}"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
    </StackPanel>
</Window>

Here is the implementation of the value converters:

    public static class Constants
    {
        public const double LatTop = 50.0;
        public const double LatBottom = 24.0;

        public const double LongLeft = 125.0;
        public const double LongRight = 66.0;
    }

    public class LatValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double latitude = (double)value;
            double height = (double)parameter;

            int top = (int)((Constants.LatTop - latitude) / (Constants.LatTop - Constants.LatBottom) * height);
            return top;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    public class LongValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double longitude = (double)value;
            double width = (double)parameter;

            int left = (int)((Constants.LongLeft - longitude) / (Constants.LongLeft - Constants.LongRight) * width);
            return left;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

The end result is that the city names are displayed at their proper locations on the canvas.  Note that because the cities are displayed in a ListBox, we can still select one of them.

999-001

#886 – Wrapping a Canvas in a ScrollViewer

Because the ScrollViewer control is a ContentControl, it can contain any single element.  It most often contains a single Panel, which in turn contains child elements.

Below is an example of a ScrollViewer that contains a Canvas, which in turn contains several different elements.  When the containing window is sized to be smaller than the Canvas element, the scrollbars automatically appear.

    <ScrollViewer HorizontalScrollBarVisibility="Auto"
                  VerticalScrollBarVisibility="Auto">
        <Canvas Width="340" Height="330">
            <Image Canvas.Left="15" Canvas.Top="5"
                   Source="Augustus.jpg" Height="100" Margin="10"/>
            <Label Canvas.Left="0" Canvas.Top="120"
                Grid.Column="1" Content="Augustus - 63BC - 14AD" />

            <Image Canvas.Top="180" Canvas.Left="200"
                   Grid.Column="2" Source="Tiberius.jpg" Height="100" Margin="10"/>
            <Label Canvas.Top="295" Canvas.Left="180"
                   Grid.Column="3" Content="Tiberius - 42BC - 37AD"/>

            <Line Canvas.Left="140" Canvas.Top="140" X2="65" Y2="50"
                  Stroke="RoyalBlue"/>
        </Canvas>
    </ScrollViewer>

886-001

#808 – How Shape Elements Are Positioned within a Canvas

The different Shape elements (e.g. Ellipse, Line, Path, Polygon, Polyline or Rectangle) describe a shape to be drawn using an X,Y coordinate system, with X increasing from left to right and Y increasing from top to bottom.

When you add Shape elements to a Canvas, the coordinates specified for the elements are used to determine the element’s position within the coordinate space of the Canvas.  For example, a point in a shape at (0,0) would be located in the upper left corner of the Canvas.

    <Canvas Margin="10" Background="AliceBlue">
        <Polygon Points="10,10 60,60 60,100 140,80 120,40"
                 Stroke="DarkViolet" StrokeThickness="2"/>
    </Canvas>

808-001
If you set any of attached properties for positioning with the Canvas (Left, Top, Right, Bottom), these properties will be used to offset the entire shape, relative to one (or more) of the sides of the canvas.

    <Canvas Margin="10" Background="AliceBlue">
        <Polygon Canvas.Left="50" Canvas.Bottom="0"
                 Points="10,10 60,60 60,100 140,80 120,40"
                 Stroke="DarkViolet" StrokeThickness="2"/>
    </Canvas>

808-003

#807 – Setting the Position of Child Elements in a Canvas from Code

Recall that you position child elements in a Canvas panel by setting at most two out of four of the following attached properties: Left, Right, Top, Bottom.  In all cases, you set a property to a value expressed in WPF (device-independent) units, equivalent to 1/96 inch.

You set any of these four values from code using one of the following static methods of the Canvas property:

  • Canvas.SetLeft
  • Canvas.SetRight
  • Canvas.SetTop
  • Canvas.SetBottom
        // Whenever we move mouse over label, put it somewhere else
        private void Label_MouseMove(object sender, MouseEventArgs e)
        {
            Label lbl = sender as Label;
            Canvas canv = lbl.Parent as Canvas;

            Random rand = new Random();

            // Set position to random location
            Canvas.SetLeft(sender as UIElement,
                           rand.Next((int)(canv.ActualWidth - lbl.ActualWidth)));
            Canvas.SetTop(sender as UIElement,
                          rand.Next((int)(canv.ActualHeight - lbl.ActualHeight)));
        }