Home » Source Code » WPF 3D Tab Carousel

WPF 3D Tab Carousel

maninwest
2015-02-10 04:47:03
The author
View(s):
Download(s): 0
Point (s): 1 
Category Category:
WindowsWindows OthersOthers

Description

Translated by maninwest@Codeforge Author:Fredrik Bornander@CodeProject
This article discusses how to create a 3D tab control for WPF. It will go through 3D rotation and camera calculations as well as show how to maintain a large set of controls in a smaller set of UI visualisers. Although a fair portion of the code for this article relates to the creation of 3D meshes, I'll not go into details around that, but rather try to focus the article around other areas more specific to this article.
There's a You  Tube video displaying some of the implemented features available here;
http://www.you  tube.com/watch?v=zmDHfsRENug&feature=plcp 


Background


This article has grown from a made-to-order solution I've implemented for Sacha. I initially knocked up a simple prototype, and Sacha wanted it to do some other things (like sliding window for example, discussed below) while I thought other things would be cool. In the end, it turned out to be a pretty neat control.
Using the code


Dead simple, download the source project and build it, the Bornander.UI.TabCarousel project contains a user control calledCarouselthat takes care of just about everything.
For example purposes, the file MainWindow.xaml.cs has two sections in its constructor that are to be used one at a time to test different aspects of the control.
Requirements


When implementing this control, I started out with a set of requirements:
The control must work on a set ofFrameworkElements so that pretty much any UI element can be used as a tab page.
The differentFrameworkElements should be displayed on 3D panels placed in a virtual carousel.
Navigation from one panel to another must be available through Next, Previous, and Go to specific index.
The number of 3D panels must be allowed to be less than the actualFrameworkElements (this was one of Sacha's requirements and it turned out to be a ball ache).
Transition from one tab page to another must be via animation.
The camera must attempt to position itself in 3D space in a such way as a best effort of maintaining the desired size of theFrameworkElement.


Implementation


Overview


The solution is split into three projects:
Bornander.UI.TabCarousel, this is the project containing the actual user control.
Bornander.UI.TabCarousel.Test, this is just a test project showcasing the user control.
Bornander.Wpf.Meshes, this is an extract of a larger project I'm working on, designed to simplify 3D using WPF.
Tackling the requirements


Use FrameworkElements


This is as simple as it sounds. I implemented a class calledTabto encapsulate the tab page, and this has a property calledElementwhich can be used to set anyFrameworkElementas the visual for thatTab.
public FrameworkElement Element
{ get { return element; } set {
      element = value;
      front.Visual = element;
    }
}

Thefrontprivate member is aViewport2DVisual3D.
Creating 3D panels


I wanted the tab pages to be blocks where the front side holds theFrameworkElement; this is easy to achieve using theViewport2DVisual3Dclass, but as I also wanted the block to have depth, I had to create two meshes, each mesh with its own material.
First, I created a lid-less box, all in one mesh and using a simpleDiffuseMaterial.


This is done by creating aBoxfromBornander.Wpf.Meshes, specifying that all sides except the front should be included:
boxMesh = Box.CreateBoxMesh(1, 1, depth, 
      Box.Side.Right | 
      Box.Side.Left | 
      Box.Side.Top | 
      Box.Side.Bottom | 
      Box.Side.Back);
Note that the width and height of the box is set to 1.0, that's because the correct aspect ratio (that is, the ratio that the UI element was designed with) is not actually calculated until the assignment of aFrameworkElement, and then a scale transform is calculated to achieve this.
The "lid" of the box is then created the same way, but this time, only the front is included:
visualHostMaterial = new DiffuseMaterial(Brushes.White);
visualHostMaterial.SetValue(
Viewport2DVisual3D.IsVisualHostMaterialProperty, true);


visualMesh = Box.CreateBoxMesh(1, 1, depth, Box.Side.Front);


front = new Viewport2DVisual3D
{
    Geometry = visualMesh,
    Visual = element,
    Material = visualHostMaterial
};

The visual host material is required to display aUIElementas an interactive material on a 3D surface. These two meshes are then added into a model of typeModelVisual3D; that way, whenever I need to move, rotate, or scale the meshes, I can simply apply the transformations to that group of meshes and not have to do it separately for each mesh.
The wholeTabclass looks like this:
class Tab
{ private readonly Material visualHostMaterial; private readonly MeshGeometry3D boxMesh; private readonly MeshGeometry3D visualMesh; private Viewport2DVisual3D front; private ModelVisual3D back; private FrameworkElement element; private double depth; public ModelVisual3D Model { get; private set; } public Tab(FrameworkElement element, Color color, double depth)
    { this.element = element; this.depth = depth;


      visualHostMaterial = new DiffuseMaterial(Brushes.White);
      visualHostMaterial.SetValue(
        Viewport2DVisual3D.IsVisualHostMaterialProperty, true);


      boxMesh = Box.CreateBoxMesh(1, 1, depth, 
        Box.Side.Right | 
        Box.Side.Left | 
        Box.Side.Top | 
        Box.Side.Bottom | 
        Box.Side.Back);
      visualMesh = Box.CreateBoxMesh(1, 1, depth, Box.Side.Front);




      front = new Viewport2DVisual3D
      {
        Geometry = visualMesh,
        Visual = element,
        Material = visualHostMaterial
      };




      back = new ModelVisual3D
      {
        Content = new GeometryModel3D
        {
          Geometry = boxMesh,
          Material = new DiffuseMaterial(Brushes.CadetBlue),
        }
      };


      Model = new ModelVisual3D();


      Model.Children.Add(back);
      Model.Children.Add(front);
    } public void UpdateTransform(int index, double angle, double radius)
    {
      TranslateTransform3D translaslation = new TranslateTransform3D( 0, 0, radius - depth / 2.0);
      
      RotateTransform3D rotation = new RotateTransform3D( new AxisAngleRotation3D(new Vector3D(0, 1, 0), -index * angle));


      ScaleTransform3D scale = element != null ? new ScaleTransform3D(1.0, double.IsNaN(element.Height) 
          ? 1.0 : 
            element.Height / element.Width, 1.0) 
          : new ScaleTransform3D(1, 1, 1);


      Transform3DGroup transform = new Transform3DGroup();


      transform.Children.Add(scale);
      transform.Children.Add(translaslation);
      transform.Children.Add(rotation);


      Model.Transform = transform;
    } public FrameworkElement Element
    { get { return element; } set {
        element = value;
        front.Visual = element;
      }
    }
}

Allowing animated navigation


In order to place theTabs in a "carousel", several things have to be calculated: the angle between the different 3D panels, the specific location for a panel and the radius, and the distance from an imaginary center to the center of the panel. All these things are dynamic, and change as the number of panels change.
The first thing, the angle is easy; simply divide 360 degrees by the number of tab panels; that means that if there are three panels, they should be separated by 120 degrees each. The second thing, the specific angle for one tab is calculated using an index; theCarouseluser control keeps aIList<Tab>and the angle is calculated using the index in this list. TheTabclasses can calculate this themselves, and that's what theUpdateTransformmethod above does. It creates a rotation transform based on the angle and the index (simply multiply the angle by the index), and that transform rotates the panel to the correct slot on the carousel. The last bit is the radius; this needs to get larger and larger as the number of panels increase so that they won't overlap. As one needs to know the number of panels, this has to be calculated by theCarousel:
private static double DegreesToRadians(double degrees)
{ return (degrees / 180.0) * Math.PI;
} private double CalculateRadius()
{ double splitAngle = 360.0 / tabs.Count; switch (tabs.Count)
    { case 1: return 0.0; case 2: return 0.25; default: return 1.0 / Math.Abs(Math.Sin(DegreesToRadians(splitAngle)));
    }
}

Since all panels are 1.0 wide (this never changes; regardless of the aspect ratio, I only modify the height), I calculate the radius as 1.0 / sin(angle between panels). This isn't the optimal distance (i.e., not the smallest distance possible without overlapping), but it's guaranteed to be larger than that, plus, I think it generates a suitable distance.
In order to actually rotate from one panel to the other, I had to come up with a lot of weird calculations (mostly due to Sacha's unreasonable requirements of sliding windows and wrapping collections); it's not that much code, but it's still fairly confusing. Sacha wanted a go-to function, allowing the user to directly jump from one tab page to another, something which is easy enough to implement, but he wanted it so that it never had to rotate more than one step. That is, in the standard setting, jumping from tab 1 to 4 will rotate past 2 and 3 before getting to 4, but Sacha wanted this to directly find 4. Completely unreasonable, if you ask me.
Below is the code that handles this, but first, it's worth noting that I request rotations by queuing upSpinInstructions that tell theAnimatemethod from where and where to go.
private class SpinInstruction
{ public int From { get; private set; } public int To { get; private set; } public SpinInstruction(int from, int to)
    {
      From = from;
      To = to;
    }
}

In the standard setting, whenever a multi-step rotation is requested by the user, it's queued up as all the steps making up that rotation.
private void Animate()
{ // If no instructions are queue up  // or if we're already animating, ignore request  if (instructions.Count == 0 || isAnimating) return; // Grab the next spin instruction  SpinInstruction instruction = instructions.Peek(); bool wrapIt = false; // If the spin To target is outside the elements list,  // this is going to be a wrapping sping  if (instruction.To < 0 || instruction.To >= elements.Count)
    { // If WrapAtEnd is enabled and if the instruction  // target is a valid one accept it  if (WrapAtEnd && (instruction.To == -1 || 
                    instruction.To == elements.Count))
      { // Set wrapIt to true to indicate that this  // is a wrapping spin and then adjust the instruction to  // fit the standard logic  wrapIt = true;
        instruction = new SpinInstruction(
          instruction.From, 
          instruction.To < 0 ? elements.Count - 1 : 0);
      } else // Done animating for now, remove instruction and return  {
        instructions.Dequeue();
        isAnimating = false; return;
      }
    } // Angle between panels  double angle = 360.0 / tabs.Count; // Figure out the target index in the tabs list  int tabToIndex = AlwaysOnlyOneStep ? 
      GetSafeIndex(currentTabIndex + 
          Math.Sign(instruction.To - instruction.From)) 
          : GetSafeIndex(instruction.To); // If this is a wrapping spin, the tabToIndex can  // be set to either the first or last index  if (wrapIt)
    { if (instruction.To == 0)
        tabToIndex = 0; if (instruction.To == elements.Count - 1)
        tabToIndex = tabs.Count - 1;
    } // Unhook from visual tree if required because  // a Visual cannot have to parents  foreach (Tab owner in (from tab in tabs 
      where tab.Element == elements[instruction.To] 
        || tab.Element == elements[instruction.From] select tab))
      owner.Element = null; // Make sure the current tab contains the From element  tabs[currentTabIndex].Element = elements[instruction.From];
    tabs[currentTabIndex].UpdateTransform(currentTabIndex, 
                          angle, CalculateRadius()); // Make sure the target tab contains the To element,  // this is what allows less tab panels than elements  tabs[tabToIndex].Element = elements[instruction.To];
    tabs[tabToIndex].UpdateTransform(tabToIndex, angle, CalculateRadius());
    isAnimating = true; // The angles of the carousel for the from and to tabs  double fromAngle = currentTabIndex * angle; double toAngle = tabToIndex * angle; // If this is a wrapping spin add/remove  // a full lap otherwise the animation  // would run backwards for these cases  if (wrapIt)
    { if (instruction.To == 0)
        toAngle += 360; if (instruction.To == elements.Count - 1)
        toAngle -= 360;
    } // If this is spinning to a later element,  // but the tab index is less than the current tab index, add a lap  if (instruction.To - instruction.From > 0 && 
        tabToIndex < currentTabIndex)
      toAngle += 360; // If this is spinning to a earlier element,  // but the tab index is greater than the  // current tab index, subtract a lap  if (instruction.To - instruction.From < 0 && 
      tabToIndex > currentTabIndex)
      toAngle -= 360;


    CreateSpinAnimation(instruction, tabToIndex, fromAngle, toAngle);
}

TheCreateSpinAnimationis responsible for creating the actual animations and callingAnimateagain when the spin animation has completed.


Mid-rotation with the FlipIt flag set to true.
Calculating camera distance


In the code above, the camera distance is calculated on a tab-per-tab basis. This is because although the tabs themselves will scale to the correct aspect ratio, there's also the issue with size on screen. If, for example, a user control was designed to be displayed in 300x400, it's not enough to create a 3D box 300 wide and 400 tall, because one set of units (the first) are in pixels and the second is unit less. It's just distance in 3D, not pixels. Therefore, theCarouselhas to calculate the distance from the panel that the camera has to be at in order for the UI element to be rendered correctly. This also depends on the size of theViewport3Dcontaining all the elements.
Basically, it looks something like this:


And, in math terms: solve the distance y, where y is one leg of a square triangle made up of y itself, 0.5 (half the 3D panel width), and the hypotenuse is formed by extending the camera's field of view (or half field of view). Since we don't know the length of the hypotenuse but can figure out the angle (as it's half the field of view), we can use tan(field of view / 2.0), or in code terms:
private double CalculateCameraDistance(int index, int tabIndex)
{
    Tab tab = tabs[tabIndex]; double y = 0.5 / Math.Tan(DegreesToRadians(MainCamera.FieldOfView / 2.0)); double panelWidth = tab.Element != null ? tab.Element.Width : 1.0; double ratio = Grid3D.ActualWidth / panelWidth; return CalculateRadius() + Math.Max(ratio, 1.0) * y;
}

When y is found, multiply it with the ratio between the designed UI element width and theViewport3Dcurrent width to compensate for the size of theViewport3D. And lastly, offset it by the distance of the radius of the carousel. By taking the max of 1.0 and the calculated ratio,Math.Max(ratio, 1.0), the distance will make sure the entire width of the panel is always visible, even if theViewport3Dis smaller than the designed size of the panel.
Since most WPF user controls are designed to be used inside a window or another control, their width and height cannot always be determined (hence the need for bothWidthandActualWidthproperties found on some WPF UI elements). In order for a user control to play nice with this tab control, it's therefore important to set theMinWidth,MaxWidth, andWidthat design time.
The user control


The WPF user control that implements the carousel is calledCarousel, intuitive, eh? And, as this control is mostly about rotation and camera position calculations, the XAML for it is quite simple:
<UserControl x:Class="Bornander.UI.TabCarousel.Carousel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" SizeChanged="HandleSizeChanged"> <Grid x:Name="Grid3D" Width="Auto" Height="Auto"> <Viewport3D> <Viewport3D.Camera> <PerspectiveCamera x:Name="MainCamera" FieldOfView="90" Position="0,0,0" LookDirection="0,0,-1"/> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <AmbientLight x:Name="Ambient" Color="#808080"/> </ModelVisual3D.Content> </ModelVisual3D> <ModelVisual3D> <ModelVisual3D.Content> <DirectionalLight x:Name="Directional" Color="#FFFFFFFF" Direction="0,-1,-1"/> </ModelVisual3D.Content> </ModelVisual3D> <ModelVisual3D x:Name="CarouselContainer"/> </Viewport3D> </Grid> </UserControl>

The user control sets up a few things:
The camera; it's important that the position for the camera is at(0, 0, 0)in order to get the distance calculations right; also, the look direction has to be along the Z-axis.
Ambient light, so that not only surfaces hit by the directional light are visible.
Directional light; this is important as the scene looks "flat" without it.
CarouselContainer: this is just theModelVisual3Dused to hold all items in the carousel; this is what is actually being rotated when the carousel spins.
Points of interest


I could have had the definitions for the meshes in XAML as well, but I find it easier and more flexible to use code for this. The most complicated part was getting the wrapping rotation right, especially when there's less tab than there are elements in the carousel. This is because the way the rotation animation works, animating from 270 degrees to 360 is different than going from 270 to 0, which kind of makes sense, but still caused me some head aches as 360 and 0 are really the same.
As always, any comments on the code or the article are most welcome.

Sponsored links

File list

Tips: You can preview the content of files by clicking file names^_^
Name Size Date
Bornander.UI.TabCarousel.sln1.97 kB2009-12-26 13:54
01.96 kB
App.xaml313.00 B2009-12-26 13:42
App.xaml.cs331.00 B2009-12-26 13:38
Bornander.UI.TabCarousel.Test.csproj4.69 kB2010-01-01 20:45
Bornander.UI.TabCarousel.Test.csproj.user74.00 B2009-12-26 13:45
01.96 kB
LoginPanel.xaml1.98 kB2010-01-01 20:45
LoginPanel.xaml.cs200.00 B2010-01-01 20:45
MainWindow.xaml3.03 kB2010-01-01 21:24
MainWindow.xaml.cs2.98 kB2010-01-01 21:25
01.96 kB
AssemblyInfo.cs2.27 kB2009-12-26 13:38
Resources.Designer.cs2.82 kB2009-12-26 13:38
Resources.resx5.48 kB2009-12-26 13:38
Settings.Designer.cs1.09 kB2009-12-26 13:38
Settings.settings201.00 B2009-12-26 13:38
01.96 kB
Bornander.UI.TabCarousel.csproj3.46 kB2009-12-26 15:04
Carousel.xaml1.03 kB2009-12-28 19:39
Carousel.xaml.cs11.61 kB2009-12-28 20:57
01.96 kB
AssemblyInfo.cs1.43 kB2009-12-26 13:45
Tab.cs2.77 kB2009-12-28 20:34
01.96 kB
Bornander.Wpf.Meshes.csproj2.85 kB2009-12-26 14:15
Box.cs3.81 kB2009-12-26 14:47
Plane.cs1.65 kB2009-12-26 14:49
01.96 kB
AssemblyInfo.cs1.42 kB2009-12-26 13:51
...

Comments

(Add your comment, get 0.1 Point)
Minimum:15 words, Maximum:160 words
  • 1
  • Page 1
  • Total 1

WPF 3D Tab Carousel (22.50 kB)

Need 1 Point(s)
Your Point (s)

Your Point isn't enough.

Get 22 Point immediately by PayPal

Point will be added to your account automatically after the transaction.

More(Debit card / Credit card / PayPal Credit / Online Banking)

Submit your source codes. Get more Points

LOGIN

Don't have an account? Register now
Need any help?
Mail to: support@codeforge.com

切换到中文版?

CodeForge Chinese Version
CodeForge English Version

Where are you going?

^_^"Oops ...

Sorry!This guy is mysterious, its blog hasn't been opened, try another, please!
OK

Warm tip!

CodeForge to FavoriteFavorite by Ctrl+D