CodeForge QQ客服 CodeForge 400电话 客服电话 4006316121
首页 » 源代码 » WPF 3D Tab Carousel

WPF 3D Tab Carousel

maninwest
发布于2015-02-10 17:47:03
源码作者
浏览次数:
下载次数:0
下载所需积分:1 
源码分类 Tags:
源码分类 所属分类:
WindowsWindows开发 OthersOthers

分享有礼! 》

  • 请点击右侧的分享按钮,把本代码分享到各社交媒体。
  • 通过您的分享链接访问Codeforge,每来2个新的IP,您将获得0.1 积分的奖励。
  • 通过您的分享链接,每成功注册一个用户,该用户在Codeforge上所获得的每1个积分,您都将获得0.2 积分的分成奖励。

代码介绍

翻译 maninwest@Codeforge 作者:Fredrik Bornander@CodeProject
如何创建 WPF 的 3D 选项卡控件。这里将讲述 3D 旋转和摄像头计算以及如何在一小组UI视觉器中维护一大组控件。
可以在此查看You  Tube视频
http://www.you   tube.com/watch?v=zmDHfsRENug&feature=plcp 



使用代码


下载源项目并进行构建即可。 Bornander.UI.TabCarousel 项目包含一个名为 Carousel 的用户控件,负责所有事情。
为了演示,文件 MainWindow.xaml.cs 在构造函数中有两个部分,一次用一个来测试控件不同的方面

要求

在实现控件时,我需要满足一组要求

控件必须在一组FrameworkElements 上工作,这样很多 UI 元素可以用作选项卡页
在虚拟走马灯中的 3D  面板中应该可以显示不同的 FrameworkElements 
必须可以通过 下一步向前,转到特定目录从一个面板导航中其他面板
 必须允许3D 面板的数量少于实际 FrameworkElements 
必须通过动画从一个tab 页面过度到另一个 
相机必须在 3D 空间中找到一个位置,能够最好地维持FrameworkElement 的理想大小


实现


概述


解决方案分成 3 个项目:
Bornander.UI.TabCarousel, 包含真正的用户控件的项目.
Bornander.UI.TabCarousel.Test, 展示用户控件的测试项目
Bornander.Wpf.Meshes, 这是从我正进行的一个大项目中提取出来的,旨在简化使用WPF的 3D .
处理这些要求


用户框架元素


我实现了一个称作Tab的类来封装选项卡页面,它有一个属性Element,可以将任何FrameworkElement设置为对一个Tab 可视 
public FrameworkElement Element
{ get { return element; } set {
      element = value;
      front.Visual = element;
    }
}

front私有成员是一个Viewport2DVisual3D.
创建 3 D 面板


我想让选项卡页面成为块,其中正面盛放FrameworkElement; 这可以轻松使用Viewport2DVisual3D 类完成,但是我想让这个块有深度,我就得创建两个网格,每一个有自己的材料。,
首先,创建一个没有盖子的箱子,都在在一个网中并使用一个简单的 DiffuseMaterial.


这可通过从 Bornander.Wpf.Meshes 中创建一个箱子来完成,指定除了正面外所有边都包括:
boxMesh = Box.CreateBoxMesh(1, 1, depth, 
      Box.Side.Right | 
      Box.Side.Left | 
      Box.Side.Top | 
      Box.Side.Bottom | 
      Box.Side.Back);
注意,盒子的宽和高设置为 1.0, 因为正确的比例 ( UI 元素设计的比例) 在赋值FrameworkElement 前实际没有计算出, 之后会计算缩放转变来实现。
使用相同的方式创建箱子的盖子,但是这次只需要包含正面:
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
};

需要视觉托管材料将 UIElement 在 3D 表面显示为交互材料。这两个网格之后会添加到ModelVisual3D 类型的模型中,这样,当我需要移动、旋转或者缩放网格时,只需对网格组应用转换,不需要对每个网格单独操作。
整个 Tab类看上去这样:
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;
      }
    }
}

允许动画导航

要将Tab 放在走马灯中,需要计算:不同 3D 面板之间的角度、一个面板特定位置和半径、虚中心到面板的中心的距离。这些都是动态的,随着面板数量的更改而更改。
首先,角度计算不难。只需使用tab 面板的数量除  360 度,这意味这如果有 3 个面板,应该每个分开120 度。第二个,使用索引计算一个 tab 的特定角度;走马灯用户控件保留着一个 IList<Tab>,角度是用列表中的索引计算出的。Tab 类可以自己进行计算,而这正是上面的UpdateTransform方法做的。它基于角度和索引创建了一个旋转变换,该变换旋转面板到走马灯上正确的位置。最后是半径,随着面板数量增加,它会变得越来越大,这样才不会重叠。所以,需要知道面板的数量,可以通过Carousel 计算:
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)));
    }
}

由于所有面板都是 1.0 宽 (不论纵横比如何,这个永远不变,我只更改高度)。我计算半径为 1.0 / sin(面板间的角度)。这不是最优距离  (即,不是不重叠的最小距离), 但是能确保比最小距离大,另外,我认为这样能带来合适的距离。
为了能够从一个面板导航至另一个,我得进行很多计算; 没有太多代码,但是仍然非常费解。 Sacha 想要一个 go-to 函数,让用户直接从一个tab 页面转到另一个,这本是容易实现的,但是他希望旋转不能超过一步,就是说,在标准设置中,从tab 1跳到 4将旋转并经过  2 和3 ,然后进入4,但是 Sacha 想直接到4. 我觉得这实在是不合理。
以下是处理这个问题的代码,首先,我通过排队 SpinInstructions 请求旋转,它会告诉 Animate方法从哪里到哪里。
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;
    }
}

在标准设置中,当用户要求多步旋转时, 它会排队,所有步完成一个旋转
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);
}

CreateSpinAnimation 负责创建真正的动画并在旋转动画完成后再次调用 Animate


Mid-rotation with the FlipIt flag set to true.
计算相机距离


在上面的代码中,基于一个个 tab 计算了相机距离。这是因为尽管tab 本身会缩放到正确的比例,在屏幕上还有大小问题。如果一个用户控件设计的显示是 300x400, 就不足以创建一个 300宽  400高 的 3D 盒子,因为一组单位是像素一组是无单位。. 在3D中只有距离没有像素。所以走马灯类得计算离相机所在的面板上的距离,以便让 UI 元素正确渲染。这也跟包含所有元素的Viewport3D的大小有关。
基本上,看上去这样:



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;
}

找出y i后,乘以设计的 UI 元素宽和Viewport3D 当前宽的比例,补充Viewport3D 的大小。最后,偏移走马灯半径的距离。通过最大值 1.0 和计算的比例,Math.Max(ratio, 1.0), 得到的距离会确保面板的整个宽度总是可见的,即使 Viewport3D 小于面板设计大小也不影响。
由于多数  WPF 用户控件设计为可以在窗口内使用,或者另一个控件内使用,他们的宽和高不总是是决定了的。为了让用户控件和tab控件能合作愉快,需要在设计时设置 MinWidth,MaxWidth, 和Width。
用户控件


实现走马灯的 WPF 用户控件叫做Carousel,因为这个控件大多数工作是旋转和计算相机。它的 XAML 很简单:
<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>

用户控件设置的一些东西:
相机; 相机的位置必须在(0, 0, 0),这样才能正确计算距离,而且看的方向应该沿着 Z 轴
环境光,这样不照到定向光源的表面也可以看见。 
定向光; 这个很重要,如果没有它就会让场景看起来缺乏立体感
CarouselContainer: 这只是 ModelVisual3D,用于在走马灯中盛放所有项目,这是当走马灯旋转时,真正被转动的东西

Sponsored links

源码文件列表

温馨提示: 点击源码文件名可预览文件内容哦 ^_^
...
名称 大小 修改日期
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
程序员商城

资源评论

(提交有效评论获得积分)
评论内容不能少于15个字,不要超出160个字。
  • 1
  • 第1页
  • 共1页

WPF 3D Tab Carousel (22.50 kB)

需要 1 积分
您持有 积分

CodeForge积分(原CF币)全新升级,功能更强大,使用更便捷,不仅可以用来下载海量源代码马上还可兑换精美小礼品了 了解更多

您的积分不足,优惠套餐快速获取 30 积分

订单支付完成后,积分将自动加入到您的账号。以下是优惠期的人民币价格,优惠期过后将恢复美元价格。

更多付款方式:网银PayPal

上传代码,免费获取CodeForge积分

您本次下载所消耗的积分将转交上传作者。

同一源码,30天内重复下载,只扣除一次积分。

登录 CodeForge

还没有CodeForge账号? 立即注册
关注微博
联系客服

Switch to the English version?

Yes
CodeForge 英文版
No
CodeForge 中文版

完善个人资料,获价值¥30元积分奖励!

^_^"呃 ...

Sorry!这位大神很神秘,未开通博客呢,请浏览一下其他的吧
好的