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:
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:
- Using
Enum
-type properties as ObjectDataProviders
for ListBox
and ComboBox
.
- Using
DrawingImage
with GeometryDrawing
and LinearGradientBrush
as an ImageSource
.
- Using
DrawingImage
with GeometryDrawing
and VisualBrush
as an ImageSource
.
The second test group shows SlidingImageControl
classes and its mouse interaction:
- Using a code-behind generated
BitmapSource
with a RenderTargetBitmap
as an ImageSource
.
- Using six transparent bitmap images of identical size but different DPIs (Dots Per Inch).
- Using one bitmap sliding in both directions.
The most important parts of the SlidingImageBase
class:
private Size CalculateImageSize()
{
double Sx = 1.0;
double Sy = 1.0;
BitmapSource aBitmapSource = ImageSource as BitmapSource;
if (aBitmapSource != null) {
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);
}
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;
}
}
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);
}
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);
}
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:
- Microsoft: DrawingBrush Class - Examples of usage
- Marco Zhou: Windows Presentation Foundation FAQ - 7.1 How to use RenderTargetBitmap
- Dwayne Need: Blurry Bitmaps – Image Snapping to Pixels
- David Owens: Backgrounds with Style
- Jose Fajardo: Dobby the Penguin
- Jason Kemp: The Missing .NET #7 - Displaying Enums in WPF
- Dr. WPF: Making the slider slide with one click anywhere on the slider
- 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.