|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis article reviews a custom WPF panel, named BackgroundWPF provides excellent support for creating 2D and 3D user interfaces. As WPF matures, these two realms of programming are mixing, allowing 3D scenes to display interactive 2D elements. Despite the progress seen thus far, some seemingly basic concepts are still not supported out-of-the-box by WPF. One of those artificial limitations is that there is no built-in support for hosting a panel's child elements in 3D space. This article shows how to get around that limitation, by using my Brief History of Panel3DI certainly cannot claim to be the sole inventor of this custom 3D panel. Not too long ago, my friend and fellow WPF Disciple, Sacha Barber, asked me to review a WPF project of his. He was working on a way to create a custom panel that painted its child elements in a After a couple trials and tribulations, I figured out a hacky way to do it. I cloned the panel's child elements and hosted the clones in a
Introducing Panel3D
The class also exposes a bubbling routed event named Using Panel3DYou can easily create a <pnl3D:Panel3D xmlns:pnl3D="clr-namespace:Panel3DLib;assembly=Panel3DLib">
<TextBox
AcceptsReturn="True"
MaxLines="8"
Text="Howdy"
Width="100" Height="100"
/>
<Button Width="100" Height="100">Destroy Universe</Button>
<CheckBox IsChecked="True">Is this cool?</CheckBox>
</pnl3D:Panel3D>
That XAML looks exactly like the XAML seen for adding elements to any other WPF panel. Since
If you decide that you would like to use <ItemsControl
xmlns:pnl3D="clr-namespace:Panel3DLib;assembly=Panel3DLib"
ItemsSource="{Binding Path=FooCollection}"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<pnl3D:Panel3D />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
The snippet seen above uses the standard You can make use of some more advanced features of <ItemsPanelTemplate x:Key="Across.Right">
<pnl3D:Panel3D
DefaultAnimationLength="0:0:0.5"
ItemLayoutDirection="3.8, .55, -6.831"
>
<pnl3D:Panel3D.Camera>
<PerspectiveCamera
LookDirection="3, 0.8, -10"
Position="+1.75, 0, 5"
UpDirection="0, 1, 0"
/>
</pnl3D:Panel3D.Camera>
</pnl3D:Panel3D>
</ItemsPanelTemplate>
When you apply those settings and view
As seen above, adjusting the
Putting 2D Elements into 3D SpaceOne of the fundamental tasks Here is the primary method responsible for this aspect of protected override void OnLogicalChildrenChanged(
UIElement elementAdded, UIElement elementRemoved)
{
// Do not create a model for the Viewport3D.
if (elementAdded == _viewport)
return;
bool add =
elementAdded != null &&
!_elementTo3DModelMap.ContainsKey(elementAdded);
if (add)
this.AddModelForElement(elementAdded);
bool remove =
elementRemoved != null &&
_elementTo3DModelMap.ContainsKey(elementRemoved);
if (remove)
this.RemoveModelForElement(elementRemoved);
}
That method overrides a method inherited from Dr. WPF's void AddModelForElement(UIElement element)
{
var model = BuildModel(element);
// Add the new model at the correct location in our list of models.
int idx = base.Children.IndexOf(element);
_models.Insert(idx, model);
_elementTo3DModelMap.Add(element, model);
// If the scene has more than just a light source, grab the first
// element and use it as the front model. Otherwise, the scene
// does not have any of our models in it yet, so pass the new one.
var frontModel =
_viewport.ModelCount > 0 ?
_viewport.FrontModel :
model;
this.BuildScene(frontModel);
}
We will not bother looking at what happens when a logical child is removed, since it is essentially the opposite of the method seen above. The method that actually creates a new 3D model is invoked at the top of the preceding method. Now let us turn our attention to that logic: /// <summary>
/// Returns an interactive 3D model that hosts
/// the specified UIElement.
/// </summary>
Viewport2DVisual3D BuildModel(UIElement element)
{
var model = new Viewport2DVisual3D
{
Geometry = new MeshGeometry3D
{
TriangleIndices = new Int32Collection(
new int[] { 0, 1, 2, 2, 3, 0 }),
TextureCoordinates = new PointCollection(
new Point[]
{
new Point(0, 1),
new Point(1, 1),
new Point(1, 0),
new Point(0, 0)
}),
Positions = new Point3DCollection(
new Point3D[]
{
new Point3D(-1, -1, 0),
new Point3D(+1, -1, 0),
new Point3D(+1, +1, 0),
new Point3D(-1, +1, 0)
})
},
Material = new DiffuseMaterial(),
Transform = new TranslateTransform3D(),
// Host the element in the 3D object.
Visual = element
};
Viewport2DVisual3D.SetIsVisualHostMaterial(model.Material, true);
return model;
}
That method creates and configures a Virtualization of the 3D SceneUI virtualization is a standard technique for improving performance when dealing with hundreds or thousands of items in a list. The idea is that you only create and keep around UI elements for the items that are currently in view. When you bring an item into view, UI elements are created for it. When scrolled out of view, the UI elements for that item are thrown away. This can have a huge impact on the memory footprint required to display a large number of items, since usually only a small fraction of them are in view at any given moment.
I decided to limit the number of items that the viewport can display to ten, so that item movement is much faster. When one model moves out of view, I add another to the viewport. This gives the illusion that all of the items are in the viewport. It is important to note, however, that I do not lazily create or destroy the 3D models; I simply add and remove them to/from the viewport as necessary. If you want to display ten thousand items, the memory footprint of all those 3D models may be prohibitively expensive. In that case, you will need to take on the much more challenging task of implementing a My simplistic UI virtualization scheme exists in two places. The /// <summary>
/// Tears down the current 3D scene and constructs a new one
/// where the specified model is the front object in view.
/// </summary>
void BuildScene(Viewport2DVisual3D frontModel)
{
_viewport.RemoveAllModels();
// Add in some 3D models, starting with the one in front.
var current = frontModel;
for (int i = 0; _viewport.ModelCount < this.MaxVisibleModels; ++i)
{
this.ConfigureModel(current, i);
_viewport.AddToBack(current);
current = this.GetNextModel(current);
if (_viewport.Children.Contains(current))
break;
}
}
The other piece of the virtualization puzzle is in the code that moves items in the scene. We examine that logic next. Animating 3D Models along a PathThe most challenging aspect of developing The way it works is actually quite simple. If the items are moved one position, the front or back item moves to the opposite end of the The main method of this algorithm is below: /// <summary>
/// Moves the items forward or backward over the specified animation length.
/// </summary>
public void MoveItems(int itemCount, bool forward, TimeSpan animationLength)
{
bool go = this.MoveItems_CanExecute(itemCount, forward, animationLength);
if (!go)
return;
// Prepare some flags that control this algorithm.
_abortMoveItems = false;
this.IsMovingItems = true;
// Move the 3D models to their new position in
// the Viewport3D's Children collection.
this.MoveItems_RelocateModels(itemCount, forward);
// If we are the items host of a Selector, select the first child element.
this.MoveItems_SelectFrontItem();
// Start moving the models to their new locations
// and apply the new opacity values.
this.MoveItems_BeginAnimations(forward, animationLength);
// Start the timer that ticks when the animations are finished.
this.MoveItems_StartCleanupTimer(animationLength);
}
I am not going to show all of the sub-routines involved with making the magic work. If you care to see how it works, download the source code and check out the /// <summary>
/// Invoked when the items stop moving, due to a call to MoveItems().
/// </summary>
void OnMoveItemsCompleted(object sender, EventArgs e)
{
_moveItemsCompletionTimer.Stop();
if (_abortMoveItems)
return;
// Remove any extra models from the scene.
while (this.MaxVisibleModels < _viewport.ModelCount)
_viewport.RemoveBackModel();
this.IsMovingItems = false;
if (0 < _moveItemsRequestQueue.Count)
{
MoveItemsRequest req = _moveItemsRequestQueue.Dequeue();
this.MoveItems(req.ItemCount, req.Forward, req.AnimationLength);
}
}
On a side note, Revision History
| ||||||||||||||||||||||||||||