Creating a sliding panel in WPF

WPF’s animation capabilities let you provide some nice styling to what would otherwise just be a static panel. As part of a project that displayed images I wanted to create a side-panel that would display the properties of an opened image, but rather than just have it sit there taking up space it would be nice to have a tab you could click that would make it slide in and out. This can be done by just creating an animation that changes the margin properties of the panel.

The expaned side panel
In the Windows.Resources tag for the main window I’ve added two animation Storyboards which are responsible for moving the side panel in and out. Named expandStoryboard and collapseStoryboard, these are bound to the propertiesPanel element (which is a grid and stackpanel containing all the image properties to display) and a tabButton user control which is the actual tab next to the properties panel and which should be the only part visible when the properties pane is hidden/collapsed.

    <Window.Resources>
        <Storyboard x:Key="expandStoryBoard"
                    TargetProperty="RenderTransform.(TranslateTransform.X)"
                    AccelerationRatio=".4"
                    DecelerationRatio=".4">
            <DoubleAnimation Storyboard.TargetName="sidePanel" Duration="0:0:0.6"
                             From="0">
                <DoubleAnimation.To>
                    <MultiBinding Converter="{StaticResource PanelConverter}">
                        <Binding Mode="OneWay" ElementName="propertiesPanel"
                                 Path="Width" />
                        <Binding Mode="OneWay" ElementName="tabButton" Path="Width" />
                    </MultiBinding>
                </DoubleAnimation.To>
            </DoubleAnimation>
        </Storyboard>
        <Storyboard x:Key="collapseStoryBoard"
                    TargetProperty="RenderTransform.(TranslateTransform.X)"
                    AccelerationRatio=".4"
                    DecelerationRatio=".4">
            <DoubleAnimation Storyboard.TargetName="sidePanel" Duration="0:0:0.6"
                             To="0">
                <DoubleAnimation.From>
                    <MultiBinding Converter="{StaticResource PanelConverter}">
                        <Binding Mode="OneWay" ElementName="propertiesPanel"
                                 Path="Width" />
                        <Binding Mode="OneWay" ElementName="tabButton" Path="Width" />
                    </MultiBinding>
                </DoubleAnimation.From>
            </DoubleAnimation>
        </Storyboard>
    </Window.Resources>

In order for the panel to be hidden to begin with, its margin property is offset to the right. Instead of just entering a value for the offset I’ve bound the sidePanel margin to the width of the entire panel (including the tab) so we don’t have to worry about specific widths. A ValueConverter is used to convert the width of the panel to a Thickness value required by the Margin property.

        <Grid Grid.Row="1" x:Name="mainGrid">
            <Image x:Name="mainImage" Stretch="Fill" />
            <StackPanel x:Name="sidePanel" HorizontalAlignment="Right"
                    Margin="{Binding ElementName=propertiesPanel, Path=Width,
                Converter={StaticResource MarginConverter}}">
                <StackPanel.RenderTransform>
                    <TranslateTransform />
                </StackPanel.RenderTransform>
                <StackPanel Orientation="Horizontal">
                    <local:SidePanelTab x:Name="tabButton" VerticalAlignment="Top"
                                        Margin="0,20,0,0"
                                        MouseDown="StackPanel_MouseDown" />
                    <StackPanel Orientation="Vertical" Background="LightGray">
                        <!-- Image properties displayed here -->
                    </StackPanel>
                </StackPanel>
            </StackPanel>
        </Grid>

So when the mouse is clicked on the tab section of the side panel (which is just a small user control containing the tab portion itself and ideally could be styled much nicer than this!), the resulting event handler in the main window triggers the animation, calling the expanding or collapsing animation depending on the current state of the panel

        private void StackPanel_MouseDown(object sender, MouseButtonEventArgs e)
        {
            //Handle single leftbutton mouse clicks
            if (e.ClickCount < 2 && e.LeftButton == MouseButtonState.Pressed)
            {
                //Ensure an image has been loaded
                if (imageLoaded == true)
                {
                    if (expanded == false)
                        sidePanel.BeginStoryboard((Storyboard)this.Resources["expandStoryBoard"]);
                    else
                        sidePanel.BeginStoryboard((Storyboard)this.Resources["collapseStoryBoard"]);

                    expanded = !expanded;
                }
            }
        }

The last component is the actual loading of an image and binding of image properties to those of the side panel. This is done using an a very simple ImageViewModel, created usingĀ  MVVM Light which provides a ViewModelBase class. This ensures all the property changed events will update the side panel properties when another image is loaded.

        private void CommandBinding_Open_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            //Launch the file open dialog
            OpenFileDialog dlg = new OpenFileDialog();
            dlg.Filter = "Images (*.jpg, *.png)|*.jpg*;*.png";

            if (dlg.ShowDialog() == true)
            {
                //Display the image -BitmapImage.UriSource must be in a BeginInit/EndInit block
                myBitmapImage = new BitmapImage();
                myBitmapImage.BeginInit();
                myBitmapImage.UriSource = new Uri(dlg.FileName);
                myBitmapImage.EndInit();

                FileInfo fInfo = new FileInfo(dlg.FileName);

                mViewModel.ImageName =
                    System.IO.Path.GetFileName(myBitmapImage.UriSource.LocalPath);
                mViewModel.ImageWidth = myBitmapImage.PixelWidth;
                mViewModel.ImageHeight = myBitmapImage.PixelHeight;
                mViewModel.ImageDepth = myBitmapImage.Format.BitsPerPixel;
                mViewModel.DateCreated = fInfo.CreationTime;
                mViewModel.FileType = fInfo.Extension;

                //set image source
                mainImage.Source = myBitmapImage;

                imageLoaded = true;
            }
        }

The source for this project can be downloaded here

The source for this project is now available here: https://bitbucket.org/bitsbobsetc/slidingsidepanel/src

About these ads

11 thoughts on “Creating a sliding panel in WPF

  1. Pingback: Force redraw after StoryBoard with WindowsFormsHost WPF

  2. Pingback: Force redraw after StoryBoard with WindowsFormsHost WPF | Jisku.com - Developers Network

  3. I am trying to modify your example (a big help, by the way) to work when the size of the propertiesPanel is not known until runtime (for example, to display the complete file name when it is too long to display in the fixed 250 pixel width. Any suggestions?

    • It’s been a while since I looked at this so I might not be much help! :) If you give a name to the TextBlock that holds the image name, such as textBlockImageName, and then call UpdateLayout() after opening the image, the textBlockImageName should have its real width.
      You might be able to assign the propertiesPanel.Width property to the textBlockImageName.ActualWidth (plus an additional amount to accout for the margins etc.) This would then give the propertiesPanel an actual Width value that the Storyboard animation can use.

  4. Bob,

    I was able to get it to work by indirectly binding to the ActualWidth. Since ActualWidth is read-only, it is not bindable, but I used the solution presented here ==> http://stackoverflow.com/questions/1083224/pushing-read-only-gui-properties-back-into-viewmodel to create a change notification property in the code behind and bound the margin property and the storyboards to it.

    I also simplified the binding in the storyboards since the multi-binding didn’t really do anything. This is because the tabButton does not have a Width dependency property, and in any case the translation amount is only the width of the propertiesPanel. I replaced the converter and added a ConverterParameter to indicate to the converter which margin I am animating from. That way the converter can conditionally multiply by -1, if the animation is from the bottom or right.

    Here is what my modified animation element looks like:

    In any case, your initial solution got me started. Thanks for posting it.

  5. Well, the XML that I posted in my previous got scrubbed when it was posted. If you know how I can post the XML and you are interested, let me know.

    Also the time of the post is 17 hours in the future…Weird!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s