Nerdworks logo "The nerd shall inherit the earth."

Nerdworks Blogorama

Nerdspeak

Centering elements on a canvas in WPF
Technobabble
3/26/2009 11:42:04 AM  

While I was fiddling around with WPF in general I noticed that when I dropped inane little circles and boxes onto a canvas I'd never be able to center it inside the containing canvas just right. If you try the following XAML in the excellent Kaxaml tool for instance:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="300"
        Height="300"
        Title="Centered?">
  <Grid>  
    <Canvas Background="Black" Width="300" Height="300">
      <Ellipse Width="50"
               Height="50"
               Canvas.Left="125"
               Canvas.Top="125"
               Fill="LightGray"
               x:Name="ellipse" />
    </Canvas>
  </Grid>
</Window>

Here's what you see:

Off center

Clearly the ellipse is not positioned in the center of the containing canvas even though the point (125,125) should in fact have done so. If however you set the value None for the WindowStyle attribute of the Window tag like so (note the code in bold):

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="300"
        Height="300"
        Title="Centered?"
        WindowStyle="None">
  <Grid>  
    <Canvas Background="Black" Width="300" Height="300">
      <Ellipse Width="50"
               Height="50"
               Canvas.Left="125"
               Canvas.Top="125"
               Fill="LightGray"
               x:Name="ellipse" />
    </Canvas>
  </Grid>
</Window>

Then here's what you get (you'll have to hit Alt+F4 to close this window):

Centered but no title bar

As you can probably tell, the circle seems nicely centered now. The problem therefore is the title bar. In the first XAML code snippet above I had given a dimension of 300x300 for the canvas. Accounting for the height of the title bar, this causes the canvas to actually extend beyond the window border which of course, gets clipped by the OS. In order to trim the canvas to size I decided to put it inside a dock panel and remove the explicit width/height specification. Here's what I came up with:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="300"
        Height="300"
        Title="Centered?">
  <DockPanel>  
    <Canvas Background="Black">
      <Ellipse Width="50"
               Height="50"
               Canvas.Left="125"
               Canvas.Top="125"
               Fill="LightGray"
               x:Name="ellipse" />
    </Canvas>
  </DockPanel>
</Window>

This produced a window that looked like this:

Again off center

Again, not quite in the center of the client area. The issue here is of course that now, the canvas size is not 300x300 which means the point (125,125) should not be the top left co-ordinate of the ellipse if we want it centered. Here's a little XAML that shows you the real dimensions of the canvas.

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="300"
        Height="300"
        Title="Centered?">
  <DockPanel>  
    <Canvas Background="Black" x:Name="canvas">
      <Ellipse Width="50"
               Height="50"
               Canvas.Left="125"
               Canvas.Top="125"
               Fill="LightGray"
               x:Name="ellipse" />
      <TextBlock Text="{Binding ElementName=canvas, Path=ActualWidth}"
                 Canvas.Left="10"
                 Canvas.Top="10"
                 Foreground="White"/>
      <TextBlock Text=", "
                 Canvas.Left="30"
                 Canvas.Top="10"
                 Foreground="White"/>
      <TextBlock Text="{Binding ElementName=canvas, Path=ActualHeight}"
                 Canvas.Left="38"
                 Canvas.Top="10"
                 Foreground="White"/>
    </Canvas>
  </DockPanel>
</Window>

Here's what the window that this produces looks like:

Actual dimensions of the canvas

The correct top-left co-ordinate to center the ellipse therefore is (121,108). The straightforwad solution seems to be to just handle it in code and be done with it. For example, the following code in the window class's constructor manages to do the job:

this.Loaded += (sender, e) =>
{
    Canvas.SetLeft(ellipse, (canvas.ActualWidth - ellipse.ActualWidth) / 2);
    Canvas.SetTop(ellipse, (canvas.ActualHeight - ellipse.ActualHeight) / 2);
};

But this isn't quite the WPF way of doing things and besides if you resized the window the ellipse would again be off center and you'd need to write more code to handle the window resize events and re-layout the ellipse in response. Ideally, these sort of things should be handled using the WPF binding system. At first I figured this should be really easy to do with XAML like this:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="300"
        Height="300"
        Title="Centered?">
  <DockPanel>  
    <Canvas Background="Black" x:Name="canvas">
      <Ellipse Width="50"
               Height="50"
               Canvas.Left="{Binding ElementName=canvas, Path=((ActualWidth - 50)/2)}"
               Canvas.Top="{Binding ElementName=canvas, Path=((ActualHeight - 50)/2)}"
               Fill="LightGray"
               x:Name="ellipse" />
    </Canvas>
  </DockPanel>
</Window>

Here's what this produced:

Passing expressions to binding path does not work

As it turns out one cannot use expressions for the value of the Path attribute of the Binding markup extension! Some folks over at blendables.com have however solved this problem by developing a custom WPF markup extension that allows the specification of expressions. You can get it here. But this seemed a bit extreme given the circumstances. The alternative as it happens is to use multi-binding with a custom value converter. First we write a class that implements IMultiValueConverter like so:

public class HalfValueConverter : IMultiValueConverter
{
    #region IMultiValueConverter Members

    public object Convert(object[] values,
                          Type targetType,
                          object parameter,
                          CultureInfo culture)
    {
        if (values == null || values.Length < 2)
        {
            throw new ArgumentException(
                "HalfValueConverter expects 2 double values to be passed" +
                " in this order -> totalWidth, width",
                "values");
        }

        double totalWidth = (double)values[0];
        double width = (double)values[1];
        return (object)((totalWidth - width) / 2);
    }

    public object[] ConvertBack(object value,
                                Type[] targetTypes,
                                object parameter,
                                CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

And then we use the following XAML (you cannot do this in Kaxaml because it does not support writing code-behind) to get the job done!

<Window x:Class="CenterWin.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Centered?"
        Height="300"
        Width="300"
        Background="Black"
        xmlns:local="clr-namespace:CenterWin">
    <DockPanel>
        <DockPanel.Resources>
            <local:HalfValueConverter x:Key="HalfValue" />
        </DockPanel.Resources>
        <Canvas x:Name="canvas">
            <Ellipse Width="50"
                     Height="50"
                     Fill="LightGray"
                     x:Name="ellipse">
                <Canvas.Left>
                    <MultiBinding Converter="{StaticResource HalfValue}">
                        <Binding ElementName="canvas" Path="ActualWidth" />
                        <Binding ElementName="ellipse" Path="ActualWidth" />
                    </MultiBinding>
                </Canvas.Left>
                <Canvas.Top>
                    <MultiBinding Converter="{StaticResource HalfValue}">
                        <Binding ElementName="canvas" Path="ActualHeight" />
                        <Binding ElementName="ellipse" Path="ActualHeight" />
                    </MultiBinding>
                </Canvas.Top>
            </Ellipse>
        </Canvas>
    </DockPanel>
</Window>

That's it! Now you can resize to your heart's content and the WPF binding system will take care of all the updates. Here's a screenshot of the window after resizing it a bit.

Nicely centered!
Link Comment (4)
 
Custom windows with WPF
Technobabble
3/10/2009 8:47:07 PM  

I am working on a little WPF app to build a UI for the Freebase web database and decided that the entry point to the app will be a simple textbox where the user can type in the search text. Here's what it looks like:

Screenshot of the search textbox.

While this was the look that I had envisioned I did not really expect to be able to achieve it as easily as I did with WPF! I at first started off with a simple window and set its size to what I wanted.

<Window x:Class="TextboxApp.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Freebase Search"
        Height="65"
        WindowStyle="None"
        Width="708"
        MinHeight="65"
        MinWidth="400"
        WindowStartupLocation="CenterScreen"
        x:Name="MainWindow">
...

I then plonked a DockPanel into the window with a TextBox in it.

<DockPanel Background="Transparent">
    <TextBox x:Name="txtSearch"
             FontSize="40"
             TextAlignment="Center"
             VerticalAlignment="Center"
             Foreground="Wheat"
    </TextBox>
</DockPanel>

I wanted a swanky gradient background and also get the nice rounded corners for the window. After a few attempts with less than stellar results I settled upon the following structure.

<DockPanel Background="Transparent">
    <Border CornerRadius="9,9,9,9">
        <Border.Background>
            <LinearGradientBrush StartPoint="0,0" EndPoint="2,1" x:Name="WindowBackground">
                <GradientStop Color="#CC000000"  Offset="0.0" />
                <GradientStop Color="#CCFFFFFF" Offset="1.0" />
            </LinearGradientBrush>
        </Border.Background>
        <TextBox x:Name="txtSearch"
                 FontSize="40"
                 TextAlignment="Center"
                 VerticalAlignment="Center"
                 KeyDown="txtSearch_KeyDown"
                 Background="Transparent"
                 Foreground="Wheat"
                 BorderBrush="Transparent"
                 BorderThickness="0"
                 Visibility="Hidden">
        </TextBox>
    </Border>
</DockPanel>

I now had a textbox with nice rounded corners but the window itself was a standard rectangle. A little googling revealed the pixy dust you've got to sprinkle to get it going. First you enable transparency on the window itself and make its background transparent like so (note the tags in bold below):

<Window x:Class="TextboxApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Freebase Search"
        Height="65"
        Width="708"
        WindowStyle="None"
        MinHeight="65"
        MinWidth="400"
        WindowStartupLocation="CenterScreen"
        x:Name="TheMainWindow"
        AllowsTransparency="True"
        Background="Transparent"
        ShowInTaskbar="False">

The basic idea is to make all backgrounds and borders transparent except for the Border tag in the DockPanel so that only the rouned corners in the border element are visible. Here's the modified code (again, note the stuff in bold):

<DockPanel Background="Transparent">
    <Border CornerRadius="9,9,9,9">
        <Border.Background>
            <LinearGradientBrush StartPoint="0,0" EndPoint="2,1" x:Name="WindowBackground">
                <GradientStop Color="#CC000000"  Offset="0.0" />
                <GradientStop Color="#CCFFFFFF" Offset="1.0" />
            </LinearGradientBrush>
        </Border.Background>
        <TextBox x:Name="txtSearch"
                 FontSize="40"
                 TextAlignment="Center"
                 VerticalAlignment="Center"
                 KeyDown="txtSearch_KeyDown"
                 Background="Transparent"
                 Foreground="Wheat"
                 BorderBrush="Transparent"
                 BorderThickness="0">
        </TextBox>
    </Border>
</DockPanel>

That's it! Now I had the nice custom round cornered window! I added some pizzazz with some fade-in/fade-out animation and I was in business!

Screenshot of the search textbox.

If you'd like to take a look at the code, here's the link you'll need.

Link Comment
 
blogorama home
about this blog
email the author
where on earth am i?
subscribe to mailing list
feeds Use these links for feed syndication
rss  |  atom
by category
technobabble (59)
philosophical crud (3)
irrelevant stuff (7)
archive
november, 2011 (2)
october, 2011 (1)
september, 2011 (7)
july, 2011 (3)
june, 2011 (2)
may, 2011 (3)
april, 2011 (1)
march, 2011 (1)
february, 2011 (1)
february, 2010 (1)
october, 2009 (1)
september, 2009 (1)
july, 2009 (5)
march, 2009 (2)
august, 2008 (2)
march, 2008 (1)
january, 2008 (1)
september, 2007 (2)
april, 2007 (1)
february, 2007 (2)
december, 2006 (1)
october, 2006 (1)
september, 2006 (4)
august, 2006 (3)
july, 2006 (4)
june, 2006 (3)
may, 2006 (6)
april, 2006 (2)
recent entries
Debugging existing...
Screen scraping wit...
Building an Instagr...
Building an Instagr...
Organizing your Jav...
290168 hits