Click here to Skip to main content
15,860,972 members
Articles / Desktop Programming / WPF

WPF 3D: Part 2 of n

Rate me:
Please Sign up or sign in to vote.
4.91/5 (70 votes)
14 Apr 2008CPOL14 min read 185.7K   4K   165   38
A WPF 3D Panel that allows tabbing and activates the current item.

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:

A video showcasing the attached demos

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.

Image 1

Click the image or here to view the video.

I would suggest waiting for the entire video to finish streaming, then watch it. It will make most sense that way.

The demos

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.

Architectural overview

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.

So what is the structure?

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:

Image 2

The main classes that do most of the work are :

TabPanel3D

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.

SelectedElementUserControl

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 FrameworkElements 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.

ConceptualPanel

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.

ItemsControl or standalone Panel

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:

C#
// get items control that hosts this panel and listen to its key events
ItemsControl ic = ItemsControl.GetItemsOwner(this);

//if it not part of an ItemsControl, hook straight into parent Window Events
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;
    // set initial focus to owning items control
    ic.Focus();
}

Viewable window

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:

Image 3

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

Image 4

This is all achieved by using three static positioned ModelVisual3Ds which simply get new VisualBrushes 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.

C#
/// <summary>
/// On key up, show the current item (the one at head of queue)
/// </summary>
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();
    }
}

/// <summary>
/// If keys are CTL+TAB suffle viewable window by 1
/// </summary>
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))
    {
        //raise our custom CustomClickWithCustomArgs event
        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:

Image 5

and the user may close it using the close button.

Reflections in 3D space

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 VisualBrushes 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
XML
<Viewport3D.Resources>
    <!-- This is applied as an overlay to get the 
         reflection effect, when Reflection is enabled -->
    <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:

Image 6

This is with reflection:

Image 7

This is achieved by using the following three utility methods:

C#
/// <summary>
/// Either creates a standard VisualBrush of the visual using the opacity
/// provided, or in the case where the UseReflections DP is set true, will
/// create a reflective Visual Brush of the visual
/// </summary>
/// <param name="visual">The visual to create the VisualBrush for</param>
/// <param name="opacity">The opacity to use</param>
/// <returns>A VisualBrush of the visual</returns>
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 };
}

/// <summary>
/// Creates a mesh to be used with a ModelVisual3D
/// </summary>
/// <returns>The Point3D[] array
/// representing the Mesh</returns>
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) 
                    };
    }
}


/// <summary>
/// Creates a new ModelVisual3D based on the input parameters
/// </summary>
/// <param name="visual"></param>
/// <param name="offset"></param>
/// <returns></returns>
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
            }
        }
    };
}

Limitations of the TabPanel3D

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.

Configuration rules

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.

ProblemConfiguration 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 ItemsControls that are used in "direct mode". ItemsControls 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:

C#
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");

    //add a button
    Button btn = new Button();
    btn.Margin = new Thickness(0);
    btn.Click += 
      new RoutedEventHandler(btn_Click);
    btn.Content = "btn0";
    lst3D.Items.Add(btn);
    
    //add a StackPanel
    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)
{
    //Create a Focusable Border
    // that contains the Image
    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:

XML
<ListBox x:Name="lst3D"
       HorizontalContentAlignment="Stretch"
       VerticalContentAlignment="Stretch">
<!-- 
  Tell the ItemsControl to use our custom
  3D layout panel to arrage its items.
  -->
<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.

C#
private void OnTabPanel3DLoaded(object sender, 
             RoutedEventArgs e)
{
    // Grab a reference
    // to the TabPanel3D when it loads.
    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.

C#
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)
{
    // See modified CurrentContent property
    // must remove the framework element
    // so that it no longer has a logical parent
    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:

XML
<ListBox x:Name="itemsControl"
  HorizontalContentAlignment="Stretch"
  VerticalContentAlignment="Stretch">
      . . .
</ListBox>

License

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


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
Questionhi Pin
Member 1007103622-Jul-13 11:12
Member 1007103622-Jul-13 11:12 
Questionneed help Pin
Waqas Ahmad Abbasi15-Sep-11 20:37
Waqas Ahmad Abbasi15-Sep-11 20:37 
GeneralMy vote of 5 Pin
Hry_9-May-11 22:18
Hry_9-May-11 22:18 
GeneralMy vote of 5 Pin
Slacker00722-Dec-10 23:54
professionalSlacker00722-Dec-10 23:54 
GeneralMy vote of 5 Pin
PotatisPulver23-Oct-10 10:12
PotatisPulver23-Oct-10 10:12 
GeneralGreat job! Thx Pin
cpw999cn25-Feb-10 16:05
cpw999cn25-Feb-10 16:05 
GeneralGreat Job Pin
Ikado15-May-08 4:58
Ikado15-May-08 4:58 
GeneralRe: Great Job Pin
Sacha Barber15-May-08 5:42
Sacha Barber15-May-08 5:42 
GeneralAmazing !!! Pin
smartattu12-May-08 23:57
smartattu12-May-08 23:57 
GeneralRe: Amazing !!! Pin
Sacha Barber13-May-08 2:43
Sacha Barber13-May-08 2:43 
GeneralRe: Amazing !!! Pin
smartattu13-May-08 3:54
smartattu13-May-08 3:54 
GeneralRe: Amazing !!! Pin
Ma tju14-May-08 16:42
Ma tju14-May-08 16:42 
GeneralRe: Amazing !!! Pin
Sacha Barber14-May-08 23:24
Sacha Barber14-May-08 23:24 
GeneralCool Stuff Pin
Sivastyle6-May-08 21:53
Sivastyle6-May-08 21:53 
GeneralRe: Cool Stuff Pin
Sacha Barber6-May-08 22:00
Sacha Barber6-May-08 22:00 
GeneralRe: Cool Stuff Pin
Sivastyle8-May-08 1:37
Sivastyle8-May-08 1:37 
GeneralRe: Cool Stuff Pin
Sacha Barber8-May-08 2:22
Sacha Barber8-May-08 2:22 
QuestionQuestion/Suggestion Pin
Member 282241022-Apr-08 8:30
Member 282241022-Apr-08 8:30 
GeneralRe: Question/Suggestion Pin
Sacha Barber22-Apr-08 21:11
Sacha Barber22-Apr-08 21:11 
QuestionDo you ever get to see daylight? Pin
martin_hughes20-Apr-08 11:44
martin_hughes20-Apr-08 11:44 
AnswerRe: Do you ever get to see daylight? Pin
Sacha Barber20-Apr-08 21:13
Sacha Barber20-Apr-08 21:13 
GeneralAnother job well done! Pin
Kavan Shaban15-Apr-08 15:53
Kavan Shaban15-Apr-08 15:53 
GeneralRe: Another job well done! Pin
Sacha Barber15-Apr-08 20:49
Sacha Barber15-Apr-08 20:49 
GeneralCrash! Pin
adamhill15-Apr-08 6:15
adamhill15-Apr-08 6:15 
GeneralRe: Crash! Pin
Sacha Barber15-Apr-08 20:50
Sacha Barber15-Apr-08 20:50 

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

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