Silverlight Window Control

by Cameron Albert 22. April 2009 17:08

Here is a draggable Window control I wrote in Silverlight for Perenthia. A screen shot of the Window being used can be seen at http://cameronalbert.com/post/2008/11/19/Silverlight-Game-Controls.aspx

Here is the XAML from my generics.xaml file:

 

<!-- Default style for Lionsguard.Window -->
	<Style TargetType="lg:Window">
		<Setter Property="Background" Value="#99000000" />
		<Setter Property="Foreground" Value="#FFFFFFFF" />
		<Setter Property="BorderBrush" Value="#FFC38312" />
		<Setter Property="Template">
			<Setter.Value>
				<ControlTemplate TargetType="lg:Window">
					<Grid x:Name="RootElement">
						<Border HorizontalAlignment="Stretch" Margin="0,24,0,0" Padding="4,4,4,4" VerticalAlignment="Stretch" BorderThickness="2,2,2,2" CornerRadius="0,0,4,4">
							<ContentControl x:Name="ContentElement" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
						</Border>
						<Border Height="24" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Top" CornerRadius="4,4,0,0" BorderThickness="2,2,2,0" x:Name="TitleBarElement" />
						<Button Height="20" HorizontalAlignment="Right" Margin="0,3,3,0" VerticalAlignment="Top" Width="20" Content="X" FontWeight="Bold" FontFamily="Trebuchet MS" FontSize="14" x:Name="CloseButtonElement"/>
						<TextBlock Height="20" Margin="8,3,27,0" VerticalAlignment="Top" Text="Title" TextWrapping="Wrap" x:Name="TitleLabelElement" FontFamily="Georgia" FontSize="16" FontWeight="Bold" HorizontalAlignment="Left"/>
					</Grid>
				</ControlTemplate>
			</Setter.Value>
		</Setter>
	</Style>

 

And here is the code for the class:

 

using System;
using System.Collections.Generic;
using System.Net;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace Lionsguard
{
	[TemplatePart(Name = "RootElement", Type = typeof(FrameworkElement))]
	[TemplatePart(Name = "ContentElement", Type = typeof(ContentControl))]
	[TemplatePart(Name = "TitleBarElement", Type = typeof(Border))]
	[TemplatePart(Name = "CloseButtonElement", Type = typeof(Button))]
	[TemplatePart(Name = "TitleLabelElement", Type = typeof(TextBlock))]
	[ContentProperty("Content")]
	public class Window : Control
	{
		public FrameworkElement RootElement { get; set; }
		private ContentControl ContentElement { get; set; }
		private Border TitleBarElement { get; set; }
		private Button CloseButtonElement { get; set; }
		private TextBlock TitleLabelElement { get; set; }

		private Point MousePosition { get; set; }

		/// <summary>
		/// Gets or sets the Content of the Window.
		/// </summary>
		public UIElement Content
		{
			get { return (UIElement)GetValue(ContentProperty); }
			set { SetValue(ContentProperty, value); }
		}
		public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(UIElement), typeof(Window), new PropertyMetadata(null, new PropertyChangedCallback(Window.OnContentPropertyChanged)));
		private static void OnContentPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			(obj as Window).SetControlValues();
		}

		/// <summary>
		/// Gets or sets the Title displayed in the title bar of the window.
		/// </summary>
		public string Title
		{
			get { return (string)GetValue(TitleProperty); }
			set { SetValue(TitleProperty, value); }
		}
		public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(Window), new PropertyMetadata("Title", new PropertyChangedCallback(Window.OnTitlePropertyChanged)));
		private static void OnTitlePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			(obj as Window).SetControlValues();
		}

		/// <summary>
		/// Gets or sets a value indicating whether or not the close button of the title bar should be displayed.
		/// </summary>
		public bool ShowCloseButton
		{
			get { return (bool)GetValue(ShowCloseButtonProperty); }
			set { SetValue(ShowCloseButtonProperty, value); }	
		}
		public static readonly DependencyProperty ShowCloseButtonProperty = DependencyProperty.Register("ShowCloseButton", typeof(bool), typeof(Window), new PropertyMetadata(true, new PropertyChangedCallback(Window.OnShowCloseButtonPropertyChanged)));
		private static void OnShowCloseButtonPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			(obj as Window).SetControlValues();
		}

		/// <summary>
		/// Gets a value indicating whether or not the window is currently open and visible.
		/// </summary>
		public bool IsOpen
		{
			get { return this.Visibility == Visibility.Visible; }
		}

		/// <summary>
		/// An event that is raised when the window is closed.
		/// </summary>
		public event EventHandler Closed = delegate { };

		/// <summary>
		/// Initializes a new instance of the Lionsguard.Window class.
		/// </summary>
		public Window()
		{
			this.DefaultStyleKey = typeof(Window);
		}

		public override void OnApplyTemplate()
		{
			base.OnApplyTemplate();

			this.RootElement = base.GetTemplateChild("RootElement") as FrameworkElement;
			this.ContentElement = base.GetTemplateChild("ContentElement") as ContentControl;
			this.TitleBarElement = base.GetTemplateChild("TitleBarElement") as Border;
			this.CloseButtonElement = base.GetTemplateChild("CloseButtonElement") as Button;
			this.TitleLabelElement = base.GetTemplateChild("TitleLabelElement") as TextBlock;
			
			if (this.RootElement != null)
			{
				this.RootElement.MouseLeftButtonDown += new MouseButtonEventHandler(OnRootElementMouseLeftButtonDown);

				this.TitleBarElement.MouseLeftButtonDown += new MouseButtonEventHandler(OnTitleBarMouseLeftButtonDown);
				this.TitleBarElement.MouseEnter += new MouseEventHandler(OnTitleBarMouseEnter);
				this.TitleBarElement.MouseLeave += new MouseEventHandler(OnTitleBarMouseLeave);

				this.TitleLabelElement.MouseLeftButtonDown += new MouseButtonEventHandler(OnTitleBarMouseLeftButtonDown);
				this.TitleLabelElement.MouseEnter += new MouseEventHandler(OnTitleBarMouseEnter);
				this.TitleLabelElement.MouseLeave += new MouseEventHandler(OnTitleBarMouseLeave);

				this.CloseButtonElement.Click += new RoutedEventHandler(OnCloseButtonClick);

				this.SetControlValues();
			}
		}

		private void SetControlValues()
		{
			if (this.ContentElement != null && this.Content != null)
			{
				this.ContentElement.Content = this.Content;
			}
			if (this.TitleLabelElement != null)
			{
				this.TitleLabelElement.Text = this.Title;
				ToolTipService.SetToolTip(this.TitleLabelElement, this.Title);
			}
			if (this.CloseButtonElement != null)
			{
				if (this.ShowCloseButton) this.CloseButtonElement.Visibility = Visibility.Visible;
				else this.CloseButtonElement.Visibility = Visibility.Collapsed;
			}
		}

		private void OnRootElementMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
		{
			// Set the Z-Index of the window to the top most position.
			this.BringToFront();
		}

		private void OnCloseButtonClick(object sender, RoutedEventArgs e)
		{
			this.Close();
		}

		private void OnTitleBarMouseLeave(object sender, MouseEventArgs e)
		{
			this.Cursor = Cursors.Arrow;
		}

		private void OnTitleBarMouseEnter(object sender, MouseEventArgs e)
		{
			this.Cursor = Cursors.Hand;
		}

		private void OnTitleBarMouseMove(object sender, MouseEventArgs e)
		{
			Point position = e.GetPosition(null);

			// Prevent the mouse from moving outside the bounds of the parent canvas.
			if (position.X <= 30) position.X = 30;
			if (position.Y <= 30) position.Y = 30;

			Canvas parent = this.Parent as Canvas;
			if (parent != null)
			{
				if (position.X >= (parent.Width - 30)) position.X = parent.Width - 30;
				if (position.Y >= (parent.Height - 30)) position.Y = parent.Height - 30;
			}

			System.Diagnostics.Debug.WriteLine("position = {0}", position);

			double deltaX = position.X - this.MousePosition.X;
			double deltaY = position.Y - this.MousePosition.Y;
			
			Point newPosition = new Point(
				((double)this.GetValue(Canvas.LeftProperty)) + deltaX,
				((double)this.GetValue(Canvas.TopProperty)) + deltaY);

			this.SetValue(Canvas.LeftProperty, newPosition.X);
			this.SetValue(Canvas.TopProperty, newPosition.Y);

			this.MousePosition = position;
		}

		private void OnTitleBarMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
		{
			((UIElement)sender).ReleaseMouseCapture();

			this.Cursor = Cursors.Arrow;

			this.TitleBarElement.MouseLeftButtonUp -= new MouseButtonEventHandler(OnTitleBarMouseLeftButtonUp);
			this.TitleBarElement.MouseMove -= new MouseEventHandler(OnTitleBarMouseMove);
			this.TitleLabelElement.MouseLeftButtonUp -= new MouseButtonEventHandler(OnTitleBarMouseLeftButtonUp);
			this.TitleLabelElement.MouseMove -= new MouseEventHandler(OnTitleBarMouseMove);
		}

		private void OnTitleBarMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
		{
			this.MousePosition = e.GetPosition(null);
			this.Cursor = Cursors.Hand;

			((UIElement)sender).CaptureMouse();

			this.TitleBarElement.MouseLeftButtonUp += new MouseButtonEventHandler(OnTitleBarMouseLeftButtonUp);
			this.TitleBarElement.MouseMove += new MouseEventHandler(OnTitleBarMouseMove);
			this.TitleLabelElement.MouseLeftButtonUp += new MouseButtonEventHandler(OnTitleBarMouseLeftButtonUp);
			this.TitleLabelElement.MouseMove += new MouseEventHandler(OnTitleBarMouseMove);
		}

		/// <summary>
		/// Causes the current window to be re-z-indexed to the top most window.
		/// </summary>
		protected void BringToFront()
		{
			// Search the top most "Window" and swap z-indexes.
			Panel parent = this.Parent as Panel; // Panel is the base for Canvas, Grid and StackPanel.
			if (parent != null)
			{
				int currentZIndex = (int)this.GetValue(Canvas.ZIndexProperty);
				var child = (from c in parent.Children where c is Window select c as Window).OrderByDescending(c => (int)c.GetValue(Canvas.ZIndexProperty)).FirstOrDefault();
				if (child != null)
				{
					int topZIndex = (int)child.GetValue(Canvas.ZIndexProperty);
					if (topZIndex == 0) topZIndex = 1; // If the value has not been set then just default it to 1.
					if (topZIndex > currentZIndex)
					{
						this.SetValue(Canvas.ZIndexProperty, topZIndex);
						child.SetValue(Canvas.ZIndexProperty, currentZIndex);
					}
				}
			}
		}

		public void Show()
		{
			this.Visibility = Visibility.Visible;
			// If content is present show that as well.
			if (this.Content != null) this.Content.Visibility = Visibility.Visible;
		}

		public void Close()
		{
			this.Visibility = Visibility.Collapsed;
			// If content is present hide that as well.
			if (this.Content != null) this.Content.Visibility = Visibility.Collapsed;
			this.Closed(this, EventArgs.Empty);
		}

		public void ToggleWindow()
		{
			if (this.Visibility == Visibility.Visible)
			{
				this.Close();
			}
			else
			{
				this.Show();
			}
		}
	}
}

 

This is how I am using it within the Pernethia UI (The content of window is actually the Character Sheet as displayed in the screenshot from above):

Namespace: xmlns:Lionsguard="clr-namespace:Lionsguard;assembly=Lionsguard.Silverlight"

 

<Lionsguard:Window Height="375" x:Name="diagCharacterSheet" Visibility="Collapsed" Width="634" Canvas.Left="8" Canvas.Top="19" Title="Character Sheet" Style="{StaticResource WindowStyle}" d:IsLocked="True">
    		<Perenthia_Dialogs:CharacterSheetDialog x:Name="diagCharacterSheetContent" SkillChanged="diagCharacterSheetContent_SkillChanged"/>
		</Lionsguard:Window>

 

Enjoy!

 

Comments

4/27/2009 7:18:33 PM #

vish

Hi,

Awesome work. I have adopted this for some of my work. Thank you.

I think that the way "SetControlValues" function has been written is preventing the class from using another template for the window control. The content defined in the new template always gets overridden by the window's content property which if not specified is NULL and thus ends up overwritting the contents defined in the template.

I had to change it to

if (this.ContentElement != null && this.Content != null)      
            {        
                this.ContentElement.Content = this.Content;      
            }      
            
            if (this.TitleLabelElement != null && Title != null)      
            {        
                this.TitleLabelElement.Text = this.Title;        
                ToolTipService.SetToolTip(this.TitleLabelElement, this.Title);      
            }

I am not sure what the best practice on this is. The Window control's control shouldn't really be different from the ContentElement. I wonder if the ContentElement is even needed or if the window needs to be a ContentControl...??? I would like to hear your thoughts on this...

Thank You,
Vish

vish United States

4/27/2009 7:34:43 PM #

calbert

I would be interested to see why your content is being overwritten. I use a custom template, not the one defined in the generic xaml and my content is not overwritten. It probably would work just fine to derive the Window from ContentControl, I don't remember what led me down this path instead. Smile

Here is the custom style I use for the window in Perenthia:
<Style x:Key="WindowStyle" TargetType="Lionsguard:Window">
      <Setter Property="Background" Value="#99000000" />
      <Setter Property="Foreground" Value="#FFFFFFFF" />
      <Setter Property="BorderBrush" Value="#FFC38312" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="Lionsguard:Window">
            <Grid x:Name="RootElement">
              
              <Border HorizontalAlignment="Stretch" Margin="0,24,0,0" Padding="4,4,4,4" VerticalAlignment="Stretch" BorderThickness="2,2,2,2" CornerRadius="0,0,4,4" BorderBrush="{StaticResource BorderBrush}" Background="{StaticResource DialogFillBrush}">
                <ContentControl x:Name="ContentElement" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
              </Border>
              <Border Height="24" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Top" CornerRadius="4,4,0,0" BorderThickness="2,2,2,0" x:Name="TitleBarElement" Background="{StaticResource TitleBarBrush}" BorderBrush="{StaticResource BorderBrush}"/>
              <Button Height="20" HorizontalAlignment="Right" Margin="0,3,3,0" VerticalAlignment="Top" Width="20" Content="X" FontWeight="Bold" FontFamily="Trebuchet MS" FontSize="14" x:Name="CloseButtonElement" Style="{StaticResource XButtonStyle}" Foreground="{StaticResource TextAltBrush}"/>
              <TextBlock Height="20" Margin="8,3,27,0" VerticalAlignment="Top" Text="Title" TextWrapping="Wrap" x:Name="TitleLabelElement" FontFamily="Georgia" FontSize="16" FontWeight="Bold" HorizontalAlignment="Stretch"  Foreground="{StaticResource HeadingBrush}" />
              
            </Grid>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

calbert United States

4/27/2009 8:12:42 PM #

vish

Here is what I have in my user control that uses the Windows Control and when doing it this way, I needed that tweak i mentioned before

<UserControl.Resources>
    <ControlTemplate x:Key="WindowControlTemplate2" TargetType="SNL:Window">
            <Grid x:Name="RootElement" Background="Maroon">
                <Border HorizontalAlignment="Stretch" Margin="0,24,0,0" Background="Red"
                                Padding="4,4,4,4" VerticalAlignment="Stretch"
                                BorderThickness="2,2,2,2" CornerRadius="0,0,4,4">
                    <ContentControl x:Name="ContentElement" HorizontalContentAlignment="Stretch"
                                            VerticalContentAlignment="Stretch">
                        <StackPanel Background="AliceBlue"
                                    Orientation="Vertical">
                            <Button Content="1 2 3 4 5"/>
                            <TextBlock Text="Hey hey hey"/>
                            <Button Content="6 7 8 9 10"/>
                        </StackPanel>
                    </ContentControl>
                </Border>
                <Border Height="24" HorizontalAlignment="Stretch"
                                Margin="0,0,0,0" VerticalAlignment="Top"
                                CornerRadius="4,4,0,0" BorderThickness="2,2,2,0"
                                x:Name="TitleBarElement"/>
                <Button Height="20" HorizontalAlignment="Right"
                                Margin="0,3,3,0" VerticalAlignment="Top" Width="20"
                                Content="XXX" FontWeight="Bold" FontFamily="Trebuchet MS"
                                FontSize="14" x:Name="CloseButtonElement"/>
                <TextBlock Height="20" Margin="8,3,27,0" VerticalAlignment="Top"
                                   Text="Here goes the title" TextWrapping="Wrap" x:Name="TitleLabelElement"
                                   FontFamily="Georgia" FontSize="16" FontWeight="Bold"
                                   HorizontalAlignment="Left"/>
            </Grid>
        </ControlTemplate>
  </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="Beige">
        
        <Canvas Background="BlueViolet">
            <SNL:Window Template="{StaticResource WindowControlTemplate2}"
                        Title="Here goes the title from the window property"
                        Width="500" Height="500"></SNL:Window>
        </Canvas>
        
    </Grid>

Thank You,
Vish

vish United States

4/27/2009 8:47:15 PM #

calbert

Ah, all of my windows have some form of Content defined in the XAML so that is why I never came across that. Smile

calbert United States

4/28/2009 8:43:20 AM #

trackback

Silverlight网络寻奇 at 090428

都是Silverlight做的游戏 http://www.silverarcade.com/Games

nasa

8/5/2009 12:47:04 AM #

Jason L.

Hey,

First off - great work here! I did something very similar in WPF for a project of mine, so it's great to see you've made a "window system" work in Silverlight Smile I've made a slight modification, however, and thought I'd share. If you set MousePosition = e.GetPosition(null) on the MouseLeftButtonDown event, the window will no longer move to the center of the cursor but the window will instead move with the mouse, much like windows in Windows or Mac work. Here's a code snippet, in case you're interested in it Smile

        private void OnTitleBarElementMouseDown(object sender, MouseButtonEventArgs e) {
            ((UIElement)sender).CaptureMouse();
            MousePosition = e.GetPosition(null);

            ... snip ...
        }

        private void OnTitleBarElementMouseMove(object sender, MouseEventArgs e) {
            Point position = e.GetPosition(null);

            double deltaX = position.X - MousePosition.X;
            double deltaY = position.Y - MousePosition.Y;
            double deltaLeft = (double)GetValue(Canvas.LeftProperty);
            double deltaTop = (double)GetValue(Canvas.TopProperty);

            double iNewLeft = deltaX + deltaLeft;
            double iNewTop = deltaY + deltaTop;

            if ( iNewLeft <= 15 ) iNewLeft = 15;
            if ( iNewTop <= 15 ) iNewTop = 15;

            Canvas cnvParent = Parent as Canvas;
            if ( cnvParent != null ) {
                // Check the right side boundary
                if ( iNewLeft >= cnvParent.ActualWidth - Width - 15 ) {
                    iNewLeft = cnvParent.ActualWidth - Width - 15;
                }
                // Check the top side boundary
                if ( iNewTop >= cnvParent.ActualHeight - Height - 15 ) {
                    iNewTop = cnvParent.ActualHeight - Height - 15;
                }
            }

            SetValue(Canvas.LeftProperty, iNewLeft);
            SetValue(Canvas.TopProperty, iNewTop);

            MousePosition = position;
        }

Hope you find this as useful as I've found your post! Laughing

Jason L. United States

8/5/2009 1:08:00 AM #

calbert

Thanks for the addition! Smile

calbert United States

Powered by BlogEngine.NET 1.5.0.7
Modified Theme by Mads Kristensen

About the Author

CameronAlbert.com I am Senior Software Development Consultant specializing in Silverlight, WPF and the Microsoft .NET Framework. 

My current project Perenthia is a Silverlight multi-player game based in a fantasy world that combines text adventure games with some moderate graphics

View Cameron Albert's profile on LinkedIn
See how we're connected

Follow cameronalbert on Twitter

 

Recommended Books

Silverlight 4 Business Application Development - Beginner's Guide:

http://www.packtpub.com/microsoft-silverlight-4-business-application-development-beginners-guide/book

Microsoft Silverlight 4 Business Application Development: Beginner’s Guide