Introduction
When I started out with this article, I wanted to create a specialised Panel
that users could add items to and it would arrange these items in 3D space. My specialised Panel
would then have a viewable window (which could be less than the actual number of children of the Panel
, thus saving memory, as we only show a few items at a time) of elements, which could be cycled through using the CTRL+TAB keys.
A bit like the 3D window tabbing you get with Windows Vista. When the user lets go of the CTRL+TAB keys, I wanted to show the FrameworkElement
at the beginning of the viewable window.
So in a nutshell, that is what this article is about. I just want to let you know that this article represents one of the hardest things that I have written about, and it was not easy to achieve. This article has been somewhat a labour of love, and has drawn not only from my own knowledge, but has also benefitted from some truly great WPF masters, helping out with advice, and in some cases (Dr WPF), even helping me create code.
Of particular mention is Josh Smith (more on Josh later), and Dr WPF. This article truly would not have been possible were it not for the help that Dr WPF gave me. He has been truly exceptional in his advice, answers, and level of support. Sir, I salute you. Also of note is the fact that I am using a few of Dr WPF's classes to aid in this article. In particular, I am using his ConceptualPanel
, which you can read about at Dr WPF's amazing article, right here. If you do any development with WPF, or want to learn it, you have to read Dr WPF's article. It is probably the best read on WPF there is, in my opinion. The guy knows his stuff man, let me tell you. He "kicks ass for the lord" (Braindead, zombie flick...Zombies rock).
The following sub sections will go into this in a little more detail:
Due to the nature of what I set out to do, a static screenshot just wouldn't do the demo application any justice, so I've created this video that can be used to view what the attached demo app actually does.
If you read through to the end of this article, you will notice that there are certain rules that need to be abided by in order for this app to work as I am showing here. The best thing to do is read the rules first, then have a look at the demo projects. There are two demo projects, each doing a different thing. These are as follows:
- PopTestWindow.xaml: Which has a standalone
TabPanel3D
, and shows static (non-interactive) elements, and has a popup to show the tabbed to item.
- ShowCurrentItemWindow.xaml: Which uses the
TabPanel3D
, as part of a ListBox
, and shows how to use interactive elements within the TabPanel3D
.
When I first started this article, I had an idea and set about writing some code; I nearly had it done, but had an issue which annoyed me, and I didn't know the answer, but I knew Josh Smith. So I sent Josh an email with my code and outlined my issue. Josh couldn't solve my issue, but was apparently inspired by my idea, and promptly came back with some code for me. Josh didn't answer my original query, but did change things around a bit, and his code didn't have my original problem; so I used Josh's new method. Josh asked me if he could blog about this, and I said, yes that's fine, so you can read about Josh's versions here and here, and now Josh also has an article here. I am just glad that my idea inspired Josh.
I think Josh does his best work when he is inspired. The particular bit of inspiration that Josh originally brought to this article was that he showed me that I can host elements in an Adorner
element within an AdornerLayer
. The reason this is a good idea is that when you are developing with WPF, you can not have an element that has two parents. So using my original approach, I would have intercepted the add/remove of child elements of the specialised Panel
and then added them to the sole child (the Viewport3D
). Josh's original Adorner idea meant that the children could be added normally as the Viewport3D
was not actually a child of the Panel
but was rather drawn on the AdornerLayer
.
So thanks for that Josh.
However, since then, there has been a number of heavy conversations amongst The WPF Disciples, regarding children/parent relationships. It is a problem (believe me). So guess what? The result of all these conversations resulted in a fabulous class called "ConceptualPanel
" being written by Dr WPF, which is available right here. If you want to know more, I strongly (very strongly) suggest that you read Dr WPF's stellar article, it's great. What the good Dr has managed to achieve is truly inspiring. But more on this later.
The rest of this section is going to outline what the attached code does and how it works.
Like I said right at the start, I wanted a generic Panel
that would be equally at home used as a simple panel or as part of an ItemsControl
(say a ListBox
). If this is the case, there are a few constraints that you must abide by, but they are not show stoppers at all.
The overall class structure is as shown in the diagram below:

The main classes that do most of the work are :
The class TabPanel3D
does the positioning of the Panel
's children in 3D space. This class responds to the PreviewKeyDown
/PreviewKeyUp
events, such that when the user presses CTRL+TAB together, the current element is moved along by one place. When the CTL+TAB keys are released, the current FrameworkElement
at the head of the queue of items is cloned and the SelectedElement
event is raised such that the consumer of the SelectedElement
event can use the current FrameworkElement
to display in a popup window (say). The SelectedElement
event passes the current FrameworkElement
as part of the raised event.
The intention is that this class should be used when you are using items within the TabPanel3D
that you wish to interact with. I.e., the items have some events/commands connected. This class is a holder for a single FrameworkElement
, which should be set to the current FrameworkElement
within the TabPanel3D
. This is obtained via the SelectedElementChangedEvent
event args. As this is a UserControl, it will exist in the same window that hosts the original source of the FrameworkElement
, the TabPanel3D
essentially. Which may or may not be used as a standalone Panel
. The FrameworkElement
's events/commands will be part of the same VisualTree, and thus can be interactive. If we were to show the current FrameworkElement
in a new Window (there is a demo for this also), we wouldn't be able to use interactive elements, as they would be shown in two separate Windows, so the routed events that may have been defined for the original elements that were added to the TabPanel3D
would not have access to the new Window VisualTree. They were created in one Window, so all the event handling code would be in that Window (I am assuming this is how you would code it), so when the FrameworkElement
is shown in a new Window, there is a new VisualTree, so the events cant route there, as it's a totally separate VisualTree. To this end, if you want to show and have interactive elements, you will have to not use popup windows.
What is really happening behind the scenes is as follows:
TabPanel3D
raises its SelectedElementChanged
event, which is then used to obtain the FrameworkElement
at the start of the queue of FrameworkElement
s within the TabPanel3D
. Then a new SelectedElementUserControl
is created and has its CurrentContent
property set to the current FrameworkElement
. Then, when the user closes the SelectedElementUserControl
, the current FrameworkElement
is added back to the TabPanel3D
.
As I stated above, when I first started all this, I had been inheriting from Panel
, which led to Josh coming up with the Adorner approach (which is no longer used). The reason that the Adorner idea is no longer relevant is that the ConceptualPanel
created by Dr WPF (here) allows children to be added to a Panel
that are really conceptual children. So you run into none of the horrors you get when you are trying to use a UIElement
that is already a child of another parent element.
To cut a long story short, by making my specialized 3D Panel
inherit from Dr WPF's LogicalPanel
, all the children added are conceptual. To paraphrase the explanation from Dr WPF's article:
Clearly, what we need is a panel class that does not live by the rules of UIElementCollection
. We still want the panel to work like every other panel. We should be able to add elements to the panel, and they should automatically go into its Children
collection. But those children should not automatically become visual children or even logical children of the panel. Ideally, they should just represent a collection of disconnected visuals, as far as the framework is concerned. The term I have coined for this new relationship is "conceptual children".
Obviously, we should still be able to use the panel as an items host. This means that its Children
collection must work seamlessly with an item container generator, just as UIElementCollection
does for native panels.
Thanks Doc.
I wanted my specialized 3D Panel
to be equally at home within an ItemsControl
, or as a standalone Panel
. And it is. The only area where there needs to be some different code, to suit these different scenarios, is to do with the PreviewKeyDown
/PreviewKeyUp
event hooks. If the Panel
is part of an ItemsControl
, we need to hook into the ItemsPanel
Key events. If however the Panel
is standalone, we need to hook into the Window
level Key events.
Basically, we need to hook into the Key events of an element that is shown and can receive the key events.
This snippet of code that does this is as shown below:
ItemsControl ic = ItemsControl.GetItemsOwner(this);
if (ic == null)
{
EventManager.RegisterClassHandler(typeof(Window),
PreviewKeyDownEvent,
new System.Windows.Input.KeyEventHandler(ItemsControl_PreviewKeyDown));
EventManager.RegisterClassHandler(typeof(Window),
PreviewKeyUpEvent, new KeyEventHandler(ItemsControl_PreviewKeyUp));
}
else
{
ic.PreviewKeyDown += ItemsControl_PreviewKeyDown;
ic.PreviewKeyUp += ItemsControl_PreviewKeyUp;
ic.Focus();
}
When Josh wrote his blogs about 3D objects, he was showing all the children (although his article now uses some basic virtualization) just at different 3D space co-ordinates.
Which is fine, but what I wanted to do all along was to create a Panel
that could contain however many items it wanted (this could be loads, remember), and I would have a viewable window onto the children collection that would be cycled through.
The cycling would wrap back to the beginning when the collection end is reached. So my requirements demand that I don't show all the children at once, but rather a small viewable window. I deliberately hard coded this, this was a conscious decision; I just messed around with the 3D meshes until I got a layout I was happy with, and made that the upper limit. This number is 3. So you will only ever get three elements shown at once; these can be shuffled by one place by pressing the CTRL+Tab keys together. For example, assume we are at the beginning of the children collection. We would see the following:

So then the user presses CTRL +TAB and the viewable window is shuffled by one place:

This is all achieved by using three static positioned ModelVisual3D
s which simply get new VisualBrush
es applied on the correct index in the actual Panel
's children collection
And the following PreviewKeyDown
/PreviewKeyUp
events are used to shuffle and raise the SelectedElementChanged
with a clone of the current element.
private void ItemsControl_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (
(e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) &&
e.KeyboardDevice.IsKeyDown(Key.Tab)) ||
(e.KeyboardDevice.IsKeyDown(Key.RightCtrl) &&
e.KeyboardDevice.IsKeyDown(Key.Tab)))
{
Shuffle();
}
}
private void ItemsControl_PreviewKeyUp(object sender, KeyEventArgs e)
{
if (!e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) &&
!e.KeyboardDevice.IsKeyDown(Key.Tab)
&& !e.KeyboardDevice.IsKeyDown(Key.RightCtrl) &&
!e.KeyboardDevice.IsKeyDown(Key.Tab))
{
SelectedElementChangedEventArgs args =
new SelectedElementChangedEventArgs(SelectedElementChangedEvent,
GetCurrentElement());
RaiseEvent(args);
}
}
So when you let the tabbing stop (assuming you have configured the TabPanel3D
selected event as I have demonstrated within the demo code (ShowCurrentItemWindow.xaml)), you will see something like this, with the current FrameworkElement
that the user is free to use. When finished, the user may close the view of the FrameworkElement
which will then allow the user to go back into tab mode:

and the user may close it using the close button.
Reflections, when working with 2D elements, are fairly easy to achieve; you normally use a VisualBrush
of the original element, along with some Opacity
loveliness. But in the 3D world, things are ever so slightly different, but not much.
This is how it works (based on ElementFlow by Pavan Podila): the TabPanel3D
exposes a UseReflections
DP, which can be used to state whether reflections are to be used for the 3D hosted items. When reflections are not required, a VisualBrush
is simply created for the current Visual
(FrameworkElement
), and a mesh is created and a ModelVisual3D
is added to the contained Viewport3D
.
In the case where reflections are required (UseReflections=True
), two VisualBrush
es of the original element are created, and one that has a ScaleTransform
(to flip it upside down) is used. Then a resource that was part of the Viewport.xaml file that was loaded (as shown below) is used to create the Opacity style Brush that allows the fading of the current Visual
(FrameworkElement
) to be seen. It's quite effective.
Reflection resource from Viewport.xaml
<Viewport3D.Resources>
-->
<LinearGradientBrush x:Key="ReflectionBrush"
StartPoint="0,0"
EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0"
Color="#7F000000"/>
<GradientStop Offset=".5"
Color="Black"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Viewport3D.Resources>
This is without reflection:

This is with reflection:

This is achieved by using the following three utility methods:
private VisualBrush CreateElementReflection(Visual visual, double opacity)
{
Rectangle topRect = new Rectangle();
topRect.Width = ElementWidth;
topRect.Height = ElementHeight;
topRect.Fill = new VisualBrush(visual);
Rectangle bottomRect = null;
Rectangle overlayRect = null;
if (UseReflections)
{
bottomRect = new Rectangle();
bottomRect.Width = ElementWidth;
bottomRect.Height = ElementHeight;
VisualBrush brush = new VisualBrush(visual);
brush.Transform = new ScaleTransform(1, -1,
ElementWidth / 2, ElementHeight / 2);
bottomRect.Fill = brush;
Canvas.SetTop(bottomRect, ElementHeight);
overlayRect = new Rectangle();
overlayRect.Width = ElementWidth;
overlayRect.Height = ElementHeight;
overlayRect.Fill =
internalResources["ReflectionBrush"] as Brush;
Canvas.SetTop(overlayRect, ElementHeight);
}
Canvas canvas = new Canvas();
canvas.Width = ElementWidth;
canvas.Height = UseReflections ? ElementHeight * 2 : ElementHeight;
canvas.Children.Add(topRect);
if (UseReflections)
{
canvas.Children.Add(bottomRect);
canvas.Children.Add(overlayRect);
}
return new VisualBrush { Visual = canvas, Opacity = opacity };
}
private Point3D[] CreateMesh()
{
if (UseReflections)
{
return new Point3D[]
{
new Point3D(-0.5, -1.0, 0),
new Point3D(0.5, -1.0, 0),
new Point3D(0.5, 1.0, 0),
new Point3D(-0.5, 1.0, 0)
};
}
else
{
return new Point3D[]
{
new Point3D(-1.5, -1.5, 0),
new Point3D(1.5, -1.5, 0),
new Point3D(1.5, 1.5, 0),
new Point3D(-1.5, 1.5, 0)
};
}
}
private ModelVisual3D Build3DModel(Visual visual, double offset)
{
return new ModelVisual3D
{
Content = new GeometryModel3D
{
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(CreateMesh())
},
Material = new DiffuseMaterial
{
Brush = CreateElementReflection(visual, 1.0)
},
BackMaterial = new DiffuseMaterial
{
Brush = Brushes.DarkGray
},
Transform = new TranslateTransform3D
{
OffsetX = offset,
OffsetY = offset,
OffsetZ = offset
}
}
};
}
There are no actual limitations that I can think of, but there are a few configuration rules that must be abided by in order for it to work as intended. These are outlined below.
As I stated, Dr WPF has been very influential with this article, and based on the many many emails I have had with him, we have come up with the following special configuration rules.
Problem |
Configuration Rule |
Using the TabPanel3D within an ItemsControl .
|
You cannot use ItemsControl directly in direct mode, but you can use any subclass like ListBox , ListView , etc. The reason you cannot use ItemsControl directly is because any UIElement that you add (Button , Image , etc.) qualifies as an item container. In this case, the ItemContainerGenerator will never get involved, so all the items will already have a logical parent.
|
Adding items to the TabPanel3D within an ItemsControl subclass (such as ListBox , ListView ).
|
You cannot directly add an "item container" to an ItemsControl . The container must be generated. Otherwise, it will already have a logical parent (the ItemsControl ) when it gets added. Typically, this only affects ItemsControl s that are used in "direct mode". ItemsControl s used in "ItemsSource mode" are not typically bound to a collection of elements that qualify as containers, so they will typically work fine.
So if the ItemsControl is a ListBox , you cannot directly add a ListBoxItem . But you can add any other UIElement (Button , Image , etc.), and then the ListBoxItem will be generated for you by the ItemContainerGenerator of the ListBox . What you need to do is something like:
private void LoadCodeBasedItems()
{
AddImage("images/image01.jpg");
AddImage("images/image02.jpg");
AddImage("images/image03.jpg");
AddImage("images/image04.jpg");
AddImage("images/image05.jpg");
AddImage("images/image06.jpg");
Button btn = new Button();
btn.Margin = new Thickness(0);
btn.Click +=
new RoutedEventHandler(btn_Click);
btn.Content = "btn0";
lst3D.Items.Add(btn);
StackPanel sp = new StackPanel();
sp.Background = Brushes.Yellow;
sp.Orientation = Orientation.Vertical;
Button btn1 = new Button();
btn1.Margin = new Thickness(4);
btn1.Content = "btn1";
btn1.Click +=
new RoutedEventHandler(btn_Click);
sp.Children.Add(btn1);
Button btn2 = new Button();
btn2.Margin = new Thickness(4);
btn2.Content = "btn2";
btn2.Click +=
new RoutedEventHandler(btn_Click);
sp.Children.Add(btn2);
lst3D.Items.Add(sp);
}
private void AddImage(string uri)
{
lst3D.Items.Add(new Border
{
Child = new Image
{
Source =
new BitmapImage(new Uri(uri,
UriKind.RelativeOrAbsolute))
},
BorderBrush = Brushes.White,
BorderThickness = new Thickness(2)
});
}
|
Getting a reference to the TabPanel3D instance within an ItemsControl subclass (such as ListBox , ListView ).
|
Firstly, we can hook into the Loaded event of the TabPanel3D element:
<ListBox x:Name="lst3D"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
-->
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<tabber3D:TabPanel3D
Background="#FF313131"
ElementWidth="200"
ElementHeight="200"
Loaded="OnTabPanel3DLoaded"
/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
And then you can use this in code-behind to obtain a reference to the actual TabPanel3D instance, which can then be used within other code. This is also where you should hook into the SelectedElementChanged event, that is used as shown in the next rule.
private void OnTabPanel3DLoaded(object sender,
RoutedEventArgs e)
{
tabPanel3D = sender as TabPanel3D;
this.LoadCodeBasedItems();
tabPanel3D.SelectedElementChanged +=
new SelectedElementChangedEventHandler(
tabPanel3D_SelectedElementChanged);
lst3D.Focus();
}
|
Using interactive elements in the TabPanel3D within an ItemsControl subclass (such as ListBox , ListView ).
|
As stated within the article, you need to create a new SelectedElementUserControl , which you add the current FrameworkElement to, and then you need to remove the SelectedElementUserControl and reconnect its child back to the TabPanel3D . This is done using methods similar to the ones shown below.
private void tabPanel3D_SelectedElementChanged(
object sender,
SelectedElementChangedEventArgs e)
{
lst3D.Visibility = Visibility.Collapsed;
fe =
tabPanel3D.Children[e.SelectedElement]
as FrameworkElement;
sec = new SelectedElementUserControl();
sec.Background = tabPanel3D.Background;
sec.RemoveControl +=
new RoutedEventHandler(sec_RemoveControl);
tabPanel3D.DisconnectLogicalChild(fe);
sec.CurrentContent = fe;
gridMain.Children.Add(sec);
}
private void sec_RemoveControl(
object sender, EventArgs e)
{
FrameworkElement fe = sec.CurrentContent;
sec.CurrentContent = null;
tabPanel3D.ReconnectLogicalChild(fe);
gridMain.Children.Remove(sec);
lst3D.Visibility = Visibility.Visible;
lst3D.Focus();
}
This assumes you have a reference to the actual TabPanel3D , which was described above.
|
Making the selected element fill the entire area within the SelectedElementUserControl .
|
You must make squire that the ItemsControl subclass (such as ListBox ) has its HorizontalContentAlignment / VerticalContentAlignment set to override the defaults. This is done as follows:
<ListBox x:Name="itemsControl"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
. . .
</ListBox>
|