Click here to Skip to main content
Click here to Skip to main content

WPF Sliding Controls Collection - Part 2: Sliding Panorama Control

By , 13 Jun 2012
Rate this:
Please Sign up or sign in to vote.

SlidingPanoramaControl.Test solution output

Sliding Controls Collection

Introduction

This article is the second part of the SlidingControls series and describes the SlidingPanoramaControl, a custom WPF control developed to view and rotate 360° panoramic images all around.

Panoramic photography is a technique of photography, using specialized equipment or software, that captures images with elongated fields of view. This field can amount up to 360 ° and, hence, illustrates the surroundings around the camera location, up to the whole all-round view. Such produced pictures normally have a small height and very big width. The following example shows an image with the original size 8.504x673 pixels, scaled proportional to 600x47 pixels conform to the prescribed CodeProject image width:

Jena Panorama image scaled to 600pt width:

Small scaled Jena Panorama 360° image

There are countless examples of panoramic photographs to find on the Internet using Google Image Search. Please with the use respect particular copyright protection hints. Here is a YouTube video that explain how to take and edit panorama photographs: 360 degree Panorama Photography tips and editing. You can see that such photos are concerning its aspect ratio not suitable to be viewed in its original form. Therefore is an appropriate viewer necessary and this is also the reason why this control was developed.

The Solution

Not to believe but, with the SlidingControls you have already a solution in your hands. The simplest panoramic viewer is SlidingImageControl itself. To try this you have to create a WPF application names MyFirstPanoramicViewer and to add reference to SlidingControls to extend XAML namespace with:

xmlns:TB="clr-namespace:TB.Instruments;assembly=SlidingControls"

After that you have to:

  1. Create an instance of SlidingImageControl named panorama;
  2. Assign a link e.g. [http://upload.wikimedia.org/wikipedia/commons/8/84/Jena_Panorama.jpg] to the ImageSource;
  3. Set both ImageHorizontalAlignment and ImageVerticalAlignment to Stretch;
  4. Set SlidingDirection to Horizontal.

And that's all! Congrats, you already have made your first great panoramic image viewer.

All together look like that:

<Window x:Class="MyFirstPanoramicViewer.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:TB="clr-namespace:TB.Instruments;assembly=SlidingControls"
    Height="400" Width="600" Background="Gray"
    Title="MyFirstPanoramicViewer">

    <TB:SlidingImageControl Name="panorama"
        ImageSource="http://upload.wikimedia.org/wikipedia/commons/8/84/Jena_Panorama.jpg"
        ImageHorizontalAlignment = "Stretch"
        ImageVerticalAlignment = "Stretch"
        SlidingDirection = "Horizontal"/>
</Window>

Panorama 360° image viewer:

Panorama 360° image viewer

Of course, we won’t be satisfied with it. Therefore we creates a new custom control derived from ContentControl with a few extras like a better image in space orientation or a navigation (rotation) support.

Exposed members:

The SlidingPanoramaControl class exposes the following members.

Constructors Description
SlidingPanoramaControl() Initializes a new instance of the SlidingPanoramaControl class and sets defaults.

Properties Description
CurrentUnit Gets or sets an unit from SlidingUnitType enumeration that specifies the current unit for all unit depend properties: NorthDirection, MouseWhellDelta and Value.
IsNormalized Gets or sets a flag that determine should a value be normalised, which was changed from embedded SlidingImageControl by mouse moving or wheel rotating.
MouseWheelDelta Gets a value that indicates the amount that the panoramic image has changed with every MouseWheel event. Default value is 15 degrees.
NorthDirection Gets or sets the north direction of a panoramic image. Default value is 0 and represent the center of the picture.
PanoramaSource Gets or sets a source of the 360° panoramic image to be shown.
SlidingUnits Gets a description of all defined SlidingUnits. This is useful outside of the class for different converters.
Value Gets or sets an offset to a start image position. The start image position is the NorthDirection deviation from the center.

Fields Description
PanoramaSourceProperty Identifies the PanoramaSourceProperty dependency property.
ValueProperty Identifies the VakueProperty dependency property.

In different MSDN and StackOverflow discussions can often be read that bitmap images for ImageSource should be loaded asynchronous before assigning, to avoid to block UI, particularly if you refer to 'slow' sources, big images or bad URLs for example. This is definitely not at all necessary because ImageSource assignment works already asynchronous. Here is an elegant solution introduced how can you assign ImageSource from different sources using IsDownloaded flag without a lot of effort.

The PanoramaSource property defined in SlidingPanoramaControl.cs:

#region PanoramaSource DependencyProperty

public ImageSource PanoramaSource
{
    get { return (ImageSource)this.GetValue(PanoramaSourceProperty); }
    set { this.SetValue(PanoramaSourceProperty, (object)value); }
}

//public static readonly DependencyProperty PanoramaSourceProperty =
//    DependencyProperty.Register("PanoramaSource", typeof(ImageSource), typeof(SlidingPanoramaControl),
//        new FrameworkPropertyMetadata((ImageSource)null,
//            new PropertyChangedCallback(OnPanoramaSourcePropertyChanged)));

// We don't need to define a new DependencyProperty on this place,
// instead shared property from SlidingPanoramaControl can be used.

public static readonly DependencyProperty PanoramaSourceProperty =
    SlidingImageControl.ImageSourceProperty.AddOwner(typeof(SlidingPanoramaControl),
     new FrameworkPropertyMetadata((ImageSource)null,
            new PropertyChangedCallback(OnPanoramaSourcePropertyChanged)));

public static void OnPanoramaSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    SlidingPanoramaControl aSlidingPanorama = (SlidingPanoramaControl)d;
    BitmapSource aBitmapSource = (ImageSource)e.NewValue as BitmapSource;
    if (aBitmapSource != null)
    {
        if (aBitmapSource.IsDownloading)
        {
            if (aSlidingPanorama._LoadingIndicator != null)
                aSlidingPanorama._LoadingIndicator.Visibility = Visibility.Visible;
            return;
        }
    }

    if (aSlidingPanorama._LoadingIndicator != null)
        aSlidingPanorama._LoadingIndicator.Visibility = Visibility.Hidden;

    aSlidingPanorama.OnPanoramaSourceChanged();
}

private void OnPanoramaSourceChanged()
{
    if (_SlidingPanorama != null)
    {
        _SlidingPanorama.ImageSource = PanoramaSource;
        if (PanoramaSource == null)
        {
            // Set default for PixelMax.
            SlidingUnits.SetPixelBounds(0, SlidingUnits.PixelMax);
            return;
        }

        // Set new image width for PixelMax.
        SlidingUnits.SetPixelBounds(0, _SlidingPanorama.ImageSize.Width);

        // Recalculate NorthDirection and MouseWheelDelta:
        OnMouseWheelDeltaChanged();
        OnNorthDirectionChanged();
    }
}

#endregion

Here is the CircularProgressBar from [3] used as a loading progress indicator.

The PanoramaSource property assigned in MainWindow.xaml.cs:

// Change SlidingPanoramaControl PanoramaSource and NorthDirection properties:
private void sources_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    PanoramaSourceItem source = (PanoramaSourceItem)sources.SelectedItem;

    // The next line can be commented if you rather want
    // to see old image so long, until newer is loading.
    panorama.PanoramaSource = null;

    BitmapImage image = new BitmapImage();
    try
    {
        image.BeginInit();
        image.CacheOption = BitmapCacheOption.OnLoad;
        image.UriSource = new Uri(source.UriString, UriKind.RelativeOrAbsolute);
        image.EndInit();
    }
    catch
    {
        image = null;
    }

    panorama.PanoramaSource = image;
    panorama.NorthDirection = source.NordDirection;
}

The SlidingImageControl accept only pixels as image position units for the ImageOffset property, but we maybe want to set MouseWheelDelta and especially NorthDirection as an angle unit (deg/rad) and want to set/get the image rotation Value as an azimuth or number of cycles. To allow this we must define suitable units and must install a ValueConverter between the ImageOffset property of the embedded private SlidingImageControl and the exposed public Value property of the SlidingPanaoramaControl itself.

For the conversion we need a lot of parameters, so the first idea was to write a MultiValueConverter. But MultiValueConverters are a kind of ‘many-to-one’ converters and will be used mostly only in one direction. In our case the conversion is a typical ‘one-to-one’ - but we need both direction because SlidingImageControl can change the ImageOffset too.

Because the ValueConverter needed several properties from the SlidingPanoramaControl class as parameters and will be used only in the class, the first decision was to apply an unusual approach and implement the converter directly in the class.

The SlidingPanoramaControl as a ValueConverter for itself:

public class SlidingPanoramaControl : ContentControl, IValueConverter
{
    // ...
    private void SlidingPanoramaControl_OnLoaded(object sender, RoutedEventArgs e)
    {
        // ...
        Binding bind = new Binding("Value");
        bind.Source = this; // Set binding source.
        bind.Converter = this; // Set value converter!!!
        bind.Mode = BindingMode.TwoWay; // Force TwoWay binding;
        _SlidingPanorama.SetBinding(SlidingImageControl.ImageOffsetProperty, bind);
    }
    public object Convert(object value, Type targetType,
                         object parameter, CultureInfo culture)
    {
        // ...
    }
    public object ConvertBack(object value, Type targetType,
            object parameter, CultureInfo culture)
    {
        // ...
    }
    // ...
}

In this manner we come in the situation that these properties can be used directly and they must not be laboriously passed/transported as parameters to the converter. Finally, for this approach we must only in the SlidingPanoramaControl integrate an IValueConverter interface and implement two their methods: Convert and ConvertBack. Thus the value converter is embedded in the source and both are the same for the binding.

A ValueConverter embedded into the Source:

A ValueConverter embedded into the Source

This solution works well, however, is quite uncommon and not reusable. To avoid this, I have decided to implement a classical separate ValueConverter derived from MarkupExtension. Deriving ValueConverter from MarkupExtension enables us to use the value converter without making it a static resource. The only problem to be solved was how to get a reference of the source object in the converter. There are three possibilities: through constructor, property or parameter. The third possibility was chosen. So a reference to the source object became a ValueConverter as a ConverterParameter using the x:Reference markup extension, because we are unable to bind directly to ConverterParameter, because this is not a DependencyProperty, because Binding is not a DependencyObject.

The x:Reference markup extension is often mistakenly associated with the XAML 2009 features that can only be used from loose XAML at the time of this writing. Although x:Reference is a new feature in WPF 4, it can be used from XAML 2006 just fine as long as your project is targeting version 4 or later of the .NET Framework.

A ValueConverter with a reference to the Source as a ConverterParameter:

A ValueConverter with a reference to the Source as a ConverterParameter

There came again an unexpected problem. When using {x:Reference}, the Visual Studio designer throws an InvalidOperationException exception with the message "Service provider is missing the INameResolver service." This is a known issue No.660141 and should be resolved sometime in the future. There are on the internet attempts to handle this problem, however, no one works so properly. Here is introduced an easy and working solution which overrides x:Reference markup extension with our one called TB:Reference:

A custom Reference markup extension in SlidingUnitsExtension.cs:

[ContentProperty("Name")]
public class Reference : System.Windows.Markup.Reference
{
    public Reference() : base() { }
    public Reference(string name) : base(name) { }
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
            return null;
        return base.ProvideValue(serviceProvider);
    }
}

And last but not least, in the same ValueConverter is included a MultipleValueConverter used by XAML in a TextBlock to convert panorama angle unit into another desired unit and to show the result combined with a StringFormat. Here is the whole masterpiece:

The SlidingUnitConverterExtension super-converter:

#region SlidingUnits converters

[ValueConversion(typeof(double), typeof(double))]
public class SlidingUnitsConverterExtension : MarkupExtension, IValueConverter, IMultiValueConverter
{
    // Deriving from MarkupExtension enables you to use the value converter    
    // without making it a static resource.
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }

    private SlidingPanoramaControl _SPC;

    #region SlidingUnits conversion helper methods

    // ...

    #endregion

    #region IValueConverter Members

    public object Convert(object value, Type targetType,
                  object parameter, CultureInfo culture) // SlidingUnitType to Pixel
    {
        // ...
    }

    public object ConvertBack(object value, Type targetType,
        object parameter, CultureInfo culture) // Pixel to SlidingUnitType
    {
        // ...
    }

    #endregion

    #region IMultiValueConverter Members

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        // ...
    }


    public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
    {
        // ...
    }

    #endregion
}

#endregion

A usage of the MultiValueConverters part:

<TextBlock
    FontFamily="Segoe UI Mono"
    FontWeight="Bold"
    FontSize="11" >
    <TextBlock.Text>            
        <MultiBinding StringFormat=" Value: {0}"
            Converter="{TB:SlidingUnitsConverter}"
            ConverterParameter="{TB:Reference panorama}">
            <Binding ElementName="panorama" Path="Value"/>
            <Binding ElementName="panorama" Path="CurrentUnit"/>
            <Binding ElementName="main" Path="DisplayUnit"/>
        </MultiBinding>
</TextBlock.Text>

For changing image azimuth is in the early stages used a standard slider control. However, it was fast clear that the slider is not well suitable for this purpose. Late was introduced a new custom control named AngleSelector. The AngleSelector can endlessly change an angle, start at the X-Axe and increases clockwise.

But an azimuth should start at Y-Axe and increases clockwise too. For the adaptation was developed a new converter named LinearConverter as a markup extension with two parameters: m = slope and b = Y-Intercept. The LinearConverter convert a variable X in another Y using well known linear equation Y = m * X + b in both directions.

Images from Wikipedia used for SlidingPanoramicControl.Test solution:

  1. Jena Panorama: Homepage, Download, Author: Michael Schreiter alias ArtMechanic.

    Full resolution: 8.504 x 673 pixels, File size: 5.67 MB, MIME type: image/jpeg, Camera location: 51° 30' 29.39" N, 0° 7' 41.31" W, License: CC-BY-SA 3.0.

  2. Trafalgar Square: Homepage, Download,Author: David Iliff alias Diliff

    Full resolution: 9.932 × 2.075 pixels, File size: 5.67 MB, MIME type: image/jpeg, Camera location: 51° 30' 29.39" N, 0° 7' 41.31" W, License: CC-BY-SA 3.0.

  3. Lac de Joux: Homepage, Download,Author: Lausanne De Suisse alias 100zax

    Full resolution: 6.000 × 697 pixels, File size: 759 KB, MIME type: image/jpeg, Camera location: ??? N, ??? W, License: CC-BY-SA 3.0.

References and third-party software components:

  1. Dmitri Nesteruk: Unit Conversions in C#/WPF
  2. VCSKicks: Photoshop-Style Angle and Altitude Selectors
  3. Sacha Barber: Better WPF Circular Progress Bar
  4. MSDN Forum: Passing parameter to IValueConverter
  5. Muhammad Shujaat Siddiqi: WPF - Binding Converter Parameter
  6. Zeeshan Amjad: Range Converter Revisited
  7. Walt Ritscher: Simplify your Binding Converter with a Custom Markup Extension

Conclusion

I hope that you picked up something useful from this article. Suggestions and comments are welcome. If you like it, vote please. 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 – 5th June, 2012 – The initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Tefik Becirovic
Systems Engineer
Germany Germany
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberfredatcodeproject18-Jun-12 6:10 
GeneralRe: My vote of 5 PinmemberTefik Becirovic18-Jun-12 11:44 
GeneralMy vote of 5 PinmemberMazen el Senih11-Jun-12 4:14 
GeneralRe: My vote of 5 PinmemberTefik Becirovic12-Jun-12 22:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140415.2 | Last Updated 13 Jun 2012
Article Copyright 2012 by Tefik Becirovic
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid