Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Sliding Controls Collection – Part 1: Sliding Image Control

0.00/5 (No votes)
5 Jun 2012 1  
The Sliding Image Control is a part of Sliding Controls, a small but useful WPF custom control collection.

SlidingImageControl.Test solution output

Sliding Controls Collection

Introduction

The Sliding Image Control is a part of Sliding Controls, a small but useful WPF custom control collection that will be present through a series of articles here. Various controls in the collection uses direct or indirect the Viewport rearranging of ImageBrush patterns to show and to slide image strips.

The Goals of The Project

The first goal was to display a picture in a defined viewport as a whole series of side by side laid tiles and thus either horizontally or vertically.

The first thought was: OK, this is quite easy, I can take a picture and draw it several times in a loop at the end of the other until the viewport has felt. The problem was that every time if the viewport has changed that new number of repetitions have to be calculated and the pictures must be redrawn.

The second goal was to display the single pictures in their original size or scaled proportionally to the width or height of the viewport. If the original size is chosen, with the viewport resizing only the number of repetitions changes, but if scaled mode is chosen, not only the number but also the positions of the single tiles changes.

The third goal was to allow setting the initial adjustment of the tiles relative to the viewport itself, and to allow changing this adjustment with an offset in the real time. In this point everything becomes a little more complicated.

The second thought was: the DrawingBrush makes fine all the things listed above, but in two directions! It would be great to try forcing the DrawingBrush to draw tiles in just one direction. And the idea was born!

According to the analysis it was quite clear: a new custom control is necessary to encapsulate all the calculations.

Background

A TileBrush is an abstract WPF class derived from Brush and ancestor for ImageBrush, DrawingBrush, and VisualBrush. TileBrush is used to fill the area of a shape with a repeating pattern. When you paint an area by using a TileBrush, you use three components: content, tiles, and output area.

Content: Depending on the inherited brush child type, the content can be an Image, a Drawing, or a Visual. The SlidingImage supports only ImageBrush, which is specified by its ImageSource dependency property. An ImageSource can be BitmapSource or DrawingImage type. See the WPF Brush classes hierarchy below.

Tiles: A TileBrush produces one or more tiles. If more tiles are produced, they evenly spread out in X and Y direction. The SlidingImage does not change this behavior, but tries by manipulation of the output area to leave the impression that tiles are spread out only in one direction depending on its orientation as if they build an endless uninterrupted stripe. By changing tiles offset, we can let the strip slide.

Output Area: The output area is the area where the brush paints. It is for the SlidingImage by default a rectangle that can (depending on other properties) be equal or smaller than a container's control area itself.

WPF Brush classes hierarchy:

System.Windows.Media.Brush
        System.Windows.Media.BitmapCacheBrush
        System.Windows.Media.GradientBrush
        System.Windows.Media.SolidColorBrush
            System.Windows.Media.TileBrush
                System.Windows.Media.DrawingBrush
                                     DrawingBrush.Drawing ->
                System.Windows.Media.ImageBrush
                                     ImageBrush.ImageSource ->
                System.Windows.Media.VisualBrush
                                     VisualBrush.Visual ->


System.Windows.Media.ImageSource
        System.Windows.Interop.D3DImage
        System.Windows.Media.DrawingImage 
                             DrawingImage.Drawing ->
        System.Windows.Media.Imaging.BitmapSource
            System.Windows.Interop.InteropBitmap
            System.Windows.Media.Imaging.BitmapFrame
            System.Windows.Media.Imaging.BitmapImage
            System.Windows.Media.Imaging.CachedBitmap
            System.Windows.Media.Imaging.ColorConvertedBitmap
            System.Windows.Media.Imaging.CroppedBitmap
            System.Windows.Media.Imaging.FormatConvertedBitmap
            System.Windows.Media.Imaging.RenderTargetBitmap
            System.Windows.Media.Imaging.TransformedBitmap
            System.Windows.Media.Imaging.WriteableBitmap


System.Windows.Media.Drawing
        System.Windows.Media.DrawingGroup
        System.Windows.Media.GeometryDrawing
        System.Windows.Media.GlyphRunDrawing
        System.Windows.Media.ImageDrawing
        System.Windows.Media.VideoDrawing

System.Windows.Media.Visual
        System.Windows.Media.ContainerVisual
        System.Windows.Media.Media3D.Viewport3DVisual
	System.Windows.UIElement


Duh, all that together sees at first sight maybe a little bit confusing. But, I recommend you to make yourself familiar with the MSDN documentation about ImageBrush to be able to understand how it works. Relevant classes for this series of articles are marked bold.

 SlidingControls classes hierarchy:

System.Windows.FrameworkElement
         TB.Instruments.SlidingImageBase
             TB.Instruments.SlidingImageControl
             TB.Instruments.SlidingParallaxLayer

System.Windows.Controls.Panel
        System.Windows.Controls.Grid
            TB.Instruments SlidingParallaxControl

System.Windows.Controls.Control
    System.Windows.Controls.ContentControl
        TB.Instruments.SlidingPanoramaControl
        TB.Instruments.SlidingCompassControl

    ...

The Solution

SlidingImage is composed of two classes: a SlidingImageBase and a SlidingImageControl. The SlidingImageBase class is derived from a FrameworkElement and is the parent for the SlidingImageControl class.

Under the motto "keep it as small as possible, but not smaller" is the FrameworkElement chosen as parent. The FrameworkElement derives from UIElement and adds support for styling, tooltips, and context menus. It is the first base class that takes part in the logical tree and so it supports data binding and resource lookup. Furthermore and most important for this application, it provides support for the WPF layout system. The SlidingImageBase class implements all functionality except mouse interaction that is implemented in the SlidingImageControl.

Exposed members:

The SlidingImageBase class exposes the following members.

Constructors Description
SlidingImageBase() Initializes a new instance of the SlidingImageBase class.

Properties Description
Background Gets or sets a brush that describes the background of a SlidingImageBase.
SlidingDirection Gets or sets a SlidingDirection enumeration that specifies a sliding direction when the ImageOffset is modified.
ImageSource Gets or sets the image displayed by an ImageBrush in an image strip.
ImageHorizontalAlignment Gets or sets the horizontal alignment of the image strip in the SlidingImageBase area.
ImageVerticalAlignment Gets or sets the verticall alignment of the image strip in the SlidingImageBase area.
ImageOffset Gets or sets an offset to the image position of the content in a TileBrush tile.
ImageScale Gets the factor which scale real to actual image size.
ImageSize Gets real image size.

Events Description
ImageOffsetChanged Occurs when the ImageOffset is modified.

Methods Description
MeasureOverride(Size) Called to measure a SlidingImageBase class.
Overrides FrameworkElement.MeasureOverride(Size).
ArrangeOverride(Size) Called to arrange and size the content of a SlidingImageBase object.
Overrides FrameworkElement.ArrangeOverride(Size).
OnRender() Called to participates in rendering operations that are directed by the layout system.
Overrides UIElement.OnRemnder().

Fields Description
BackgroundProperty Identifies the Background dependency property.
ImageSourceProperty Identifies the ImageSourceProperty dependency property.
ImageHorizontalAlignmentProperty Identifies the ImageHorizontalAlignmentProperty dependency property.
ImageVerticalAlignmentProperty Identifies the ImageVerticalAlignmentProperty dependency property.
SlidingDirectionProperty Identifies the SlidingDirectionProperty dependency property.
ImageOffsetProperty Identifies the ImageOffsetProperty dependency property.

Except constructor and MouseWheelDelta property the SlidingImageControl doesn’t expose additional members.

Constructors Description
SlidingImageControl() Initializes a new instance of the SlidingImageControl class.

Properties Description
MouseWheelDelta Gets a value that indicates the amount that the ImageOffset has changed with every MouseWheel event. Default value is 120.


SlidingImage Layout:

SlidingImageControl.Test solution output

How it works

First at all we need a suitable image to be shown. The image can be a bitmap image as a BitmapSource, a vector image as a DrawingImage, or Direct3D surface as a D3DImage. After the image to the ImageSource property of SlidingImageBase class is assigned, the OnImageSourcePropertyChanged method will be called. In this method will be the corresponding ImageSize ascertained and a suitable ImageStrip of ImageBrush type assembled. After that, the MeasureOverride and the ArrangeOverride methods will be called successively. In the MeasureOverride method will be an ImageScale and an ImageStripSize/Location-pair calculated. In the ArrangeOverride method will be an ActualImageSize/Location-pair calculated and assigned to the ImageTile.Viewport.

If the ImageSource is a BitmapSource type, the ImageSize will be multiplied with the ratio of the Image DPI and Screen DPI (Dot Per Inch). Screen DPI is given from the actual desktop using the GDI GetDeviceCaps function. Note that the ImageSize can be different from the ActualImageSize if Strech alignment is used.

The SlidingImageControl.Test solution should show clearly the behavior described above. The title image features two test groups enclosed in the solution.

The first test group shows the SlidingImageBase classes and its customization with properties:

  1. Using Enum-type properties as ObjectDataProviders for ListBox and ComboBox.
  2. Using DrawingImage with GeometryDrawing and LinearGradientBrush as an ImageSource.
  3. Using DrawingImage with GeometryDrawing and VisualBrush as an ImageSource.

The second test group shows SlidingImageControl classes and its mouse interaction:

  1. Using a code-behind generated BitmapSource with a RenderTargetBitmap as an ImageSource.
  2. Using six transparent bitmap images of identical size but different DPIs (Dots Per Inch).
  3. Using one bitmap sliding in both directions.

The most important parts of the SlidingImageBase class:

        // Calculate ImageSize considering screen DPI (Dot-Per-Inch) values.
        private Size CalculateImageSize()
        {
            double Sx = 1.0;
            double Sy = 1.0;

            BitmapSource aBitmapSource = ImageSource as BitmapSource;
            if (aBitmapSource != null) // if ImageSource is a bitmap image
            {
                Sx = aBitmapSource.DpiX / 
                    (Double)DeviceHelper.PixelsPerInch(Orientation.Horizontal); 
                Sy = aBitmapSource.DpiY / 
                    (Double)DeviceHelper.PixelsPerInch(Orientation.Vertical); 
            }

            return new Size(Sx * ImageSource.Width, Sy * ImageSource.Height);
        }

        // Create a new ImageTile of the ImageBrush type
        private static void OnImageSourcePropertyChanged(DependencyObject d,
                DependencyPropertyChangedEventArgs e)
        {
            SlidingImageBase aSlidingImageBase = (SlidingImageBase)d;
            ImageSource aImageSource = (ImageSource)e.NewValue;

            if (aImageSource != null)
            {
                aSlidingImageBase._ImageSize =
                   aSlidingImageBase.CalculateImageSize();

                aSlidingImageBase._ImageTile = new ImageBrush();
                aSlidingImageBase._ImageTile.ImageSource = aImageSource;
                aSlidingImageBase._ImageTile.TileMode = TileMode.Tile;
                aSlidingImageBase._ImageTile.Stretch = Stretch.Fill;
                aSlidingImageBase._ImageTile.ViewportUnits =
                   BrushMappingMode.Absolute;
            }
            else
            {
                aSlidingImageBase._ImageTile = null;
            }
        }

        // Callculate convinient ImageTile size:
        protected override Size MeasureOverride(Size availableSize)
        {
            if (ImageSource != null)
            {
                if (SlidingDirection == SlidingDirection.Horizontal)
                {
                    if (ImageVerticalAlignment == VerticalAlignment.Stretch)
                        _ImageScale = availableSize.Height / _ImageSize.Height;
                    else
                        _ImageScale = 1.0;

                    _ImageStrip.Width = availableSize.Width;
                    _ImageStrip.Height = _ImageScale * _ImageSize.Height;

                    _ImageStrip.X = 0.0;
                    switch (ImageVerticalAlignment)
                    {
                        case VerticalAlignment.Top:
                        default:
                            _ImageStrip.Y = 0.0;
                            break;
                        case VerticalAlignment.Stretch:
                        case VerticalAlignment.Center:
                            _ImageStrip.Y = 
                                (availableSize.Height - _ImageStrip.Height) / 2.0;
                            break;
                        case VerticalAlignment.Bottom:
                            _ImageStrip.Y = 
                                availableSize.Height - _ImageStrip.Height;
                            break;
                    }
                }
                else
                {
                    if (ImageHorizontalAlignment == HorizontalAlignment.Stretch)
                        _ImageScale = availableSize.Width / _ImageSize.Width;

                    else
                        _ImageScale = 1.0;

                    _ImageStrip.Width = _ImageScale * _ImageSize.Width;
                    _ImageStrip.Height = availableSize.Height;

                    switch (ImageHorizontalAlignment)
                    {
                        case HorizontalAlignment.Left:
                        default:
                            _ImageStrip.X = 0.0;
                            break;
                        case HorizontalAlignment.Stretch:
                        case HorizontalAlignment.Center:
                            _ImageStrip.X = 
                               (availableSize.Width - _ImageStrip.Width) / 2.0;
                            break;
                        case HorizontalAlignment.Right:
                            _ImageStrip.X = 
                                availableSize.Width - _ImageStrip.Width;
                            break;
                    }
                    _ImageStrip.Y = 0.0;
                }
            }

            return base.MeasureOverride(availableSize);
        }

        // Callculate convinient ImageTile position
        protected override Size ArrangeOverride(Size finalSize)
        {
            if (ImageSource != null)
            {
                Size _ActualImageSize = 
                  new Size(_ImageScale * _ImageSize.Width, 
                           _ImageScale * _ImageSize.Height);

                Point _ActualImageLocation;
                if (SlidingDirection == SlidingDirection.Horizontal)
                {
                    double horizontalDisplacement;
                    switch (ImageHorizontalAlignment)
                    {
                        case HorizontalAlignment.Left:
                        default:
                            horizontalDisplacement = 0.0;
                            break;
                        case HorizontalAlignment.Stretch:
                        case HorizontalAlignment.Center:
                            horizontalDisplacement = 
                              (finalSize.Width - _ActualImageSize.Width) / 2.0;
                            break;
                        case HorizontalAlignment.Right:
                            horizontalDisplacement = 
                               finalSize.Width - _ActualImageSize.Width;
                            break;
                    }
                    _ActualImageLocation = new Point(_ImageScale * ImageOffset %
                        _ActualImageSize.Width + horizontalDisplacement,
                        _ImageStrip.Y);
                }
                else
                {
                    double verticalDisplacement;
                    switch (ImageVerticalAlignment)
                    {
                        case VerticalAlignment.Top:
                        default:
                            verticalDisplacement = 0.0;
                            break;
                        case VerticalAlignment.Stretch:
                        case VerticalAlignment.Center:
                            verticalDisplacement = 
                               (finalSize.Height - _ActualImageSize.Height) / 2.0;
                            break;
                        case VerticalAlignment.Bottom:
                            verticalDisplacement = 
                               finalSize.Height - _ActualImageSize.Height;
                            break;

                    }
                    _ActualImageLocation = new Point(_ImageStrip.X, _ImageScale *
                      ImageOffset % _ActualImageSize.Height + verticalDisplacement);
                }

                _ImageTile.Viewport = 
                   new Rect(_ActualImageLocation, _ActualImageSize);

            }

            return base.ArrangeOverride(finalSize);
        }

        // Redraw Background and ImageTile
        protected override void OnRender(DrawingContext dc)
        {
            Rect rect = new Rect(0.0, 0.0, this.RenderSize.Width, this.RenderSize.Height);

            if (ClipToBounds)
                dc.PushClip(new RectangleGeometry(rect));

            if (Background != null)
                dc.DrawRectangle(Background, (Pen)null, rect);

            if (_ImageTile != null)
                dc.DrawRectangle(_ImageTile, (Pen)null, _ImageStrip);

            if (ClipToBounds)
                dc.Pop();
        }


References and third-party software components:

  1. Microsoft: DrawingBrush Class - Examples of usage
  2. Marco Zhou: Windows Presentation Foundation FAQ - 7.1 How to use RenderTargetBitmap
  3. Dwayne Need: Blurry Bitmaps – Image Snapping to Pixels
  4. David Owens: Backgrounds with Style
  5. Jose Fajardo: Dobby the Penguin
  6. Jason Kemp: The Missing .NET #7 - Displaying Enums in WPF
  7. Dr. WPF: Making the slider slide with one click anywhere on the slider
  8. Walt Ritscher: Create an Auto-Centering Slider control with WPF

Conclusion

I hope that you picked up something useful from this article. Suggestions and comments are welcome. If you like it, please vote for it. If you use it, please describe your successful story in the comments below. If you wish to sell it or to distribute it commercially, please contact me.

History

  • v1.0 – 3 May, 2012 – The initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here