#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

 

Advertisement

#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)));
        }

#806 – Setting ZIndex Values of Child Elements in a Canvas from Code

You use the Canvas.ZIndex attached property on a child element in a Canvas to indicate the relative positioning of elements, when they happen to overlap.  Elements that have higher ZIndex values will appear on top of elements having lower values.

        <Button Content="1 - Lft10,Top10" Canvas.Left="10" Canvas.Top="10"
                Canvas.ZIndex="4"/>
        <Button Content="2 - Rt10,Top10" Canvas.Right="10" Canvas.Top="15"
                Canvas.ZIndex="3"/>
        <Button Content="3 - Lft10,Bott10..." Canvas.Left="15" Canvas.Bottom="15"
                Canvas.ZIndex="2"/>
        <Button Content="4 - Rt10,Bott10" Canvas.Right="10" Canvas.Bottom="8"
                Canvas.ZIndex="1"
                Click="Button_Click"/>

806-001
As with other attached properties, you can set the value from code by using a static method of the form SetPropName.  In the case of ZIndex, you use the static Canvas.SetZIndex method, passing in a reference to child control.

private void Button_Click(object sender, RoutedEventArgs e)
{
    Canvas.SetZIndex(sender as UIElement, 5);
}

#740 – Set Background of Canvas to Transparent to Receive Touch Events

If you define one or more touch event handlers for a Canvas panel, without setting any other properties, you may not see any touch events for the canvas.

This happens because none of the controls inheriting from Panel will receive either touch or mouse events unless you specify a value for the panel’s Background property.  So, to receive touch events for the Canvas, you can just set its Background property to Transparent.

    <Canvas Name="canvMain" Background="Transparent"
        TouchDown="Canvas_TouchDown" TouchMove="Canvas_TouchMove" TouchUp="Canvas_TouchUp">
    </Canvas>

#479 – Using a Layout Transform on Child Elements in a Canvas

You can use a LayoutTransform on child elements of a Canvas to transform them graphically.  Specifically, you can use a RotateTransform, ScaleTransform or SkewTransform.  (Translation transforms are ignored).

    <Canvas>
        <Button Content="Hopalong Cassidy" Canvas.Left="10" Canvas.Top="10">
            <Button.LayoutTransform>
                <RotateTransform Angle="45"/>
            </Button.LayoutTransform>
        </Button>
        <Button Content="Roy Rogers" Canvas.Right="10" Canvas.Top="10">
            <Button.LayoutTransform>
                <TransformGroup>
                    <ScaleTransform ScaleX="2.0"/>
                    <RotateTransform Angle="-45"/>
                </TransformGroup>
            </Button.LayoutTransform>
        </Button>
        <Button Content="Spade Cooley" Canvas.Left="10" Canvas.Bottom="10">
            <Button.LayoutTransform>
                <SkewTransform AngleX="20"/>
            </Button.LayoutTransform>
        </Button>
    </Canvas>