Silverlight: simulate a ‘Windows’ desktop application - part 3 (Resizable Window)

Print Content | More

It’s time to write the last chapter of this series and take a look at how the resizing capability of the Window is implemented. Since the last time some new feature were added:

  • Code ported to Silverlight 2.0 RTW.
  • Solution refactored, the control is now available as a standalone assembly.
  • The template of the Window has been revised to correct some bugs.
  • We can now resize from any side and corner of the window.
  • We have support for automatic scrolling if we set the minimum size (MinWidth and MinHeight) of the framework element contained into the window control.

Here’s the actual result:

SimulatingWindows3

Like in the previous article, in which we talked about dragging the window around, the basic idea is to subscribe to any mouse event raised by the control and to write then code there to handle the resizing.

Resizing the window is a bit more complicated than dragging it around, so more code will be required.

We start with a consideration: we want to keep our template very very simple so, to avoid having some ‘hidden’ framework elements that defines the hotspot zones that the user can use to resize the window, we just get the current mouse position and we do some computations to see if it’s near the borders or the corners of the window (hotspots); if this condition is satisfied we can identify which side or corner the user is going to resize.

Then, when the user moves the mouse around, if we are in resizing mode, we compute the new window size and position and we update the dimensions of the external control and of the inner content presenter.

Let’s start with the xaml style template (some code was stripped):

   1: <Style TargetType="windows:Window">
   2:     <Setter Property="Template">
   3:         <Setter.Value>
   4:             <ControlTemplate TargetType="windows:Window">
   5:                 <Grid Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" x:Name="PART_Window">
   6:                     <!-- Outer shadow -->
   7:                     <Border CornerRadius="4,4,4,4" Background="#22000000" Margin="-2,-2,-2,-2" />
   8:                       ...
   9:                             <!-- Content presenter for hosting the content --> <!-- HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" -->
  10:                             <ScrollViewer x:Name="PART_ScrollContent" Grid.Row="1" >
  11:                                 <ContentPresenter x:Name="PART_ContentPresenter" />
  12:                             </ScrollViewer>
  13:                       ...
  14:                     </Border>
  15:                 </Grid>
  16:             </ControlTemplate>
  17:         </Setter.Value>
  18:     </Setter>
  19: </Style>

Here we can see the main elements that are involved into the resizing thing:

  • PART_Window: the Grid that defines the layout of the control.
  • PART_ScrollContent: a ScrollViewer used to enable the automatic support for scrolling the content if it’s wider than the current size of the window (this is disabled by default).
  • PART_ContentPresenter: the placeholder for the real window content.

To support resizing we need two properties and an enum:

   1: /// <summary>
   2: /// defines where the user mouse is positioned inside the control
   3: /// </summary>
   4: private enum ResizeAnchor
   5: {
   6:     None,
   7:     Left,
   8:     TopLeft,
   9:     Top,
  10:     TopRight,
  11:     Right,
  12:     BottomRight,
  13:     Bottom,
  14:     BottomLeft
  15: }
  16:  
  17: /// <summary>
  18: /// enable/disable support for resize this window
  19: /// </summary>
  20: public bool ResizeEnabled { get { return _ResizeEnabled; } set { _ResizeEnabled = value; } }
  21: private bool _ResizeEnabled = true;
  22:  
  23: /// <summary>
  24: /// returns true if the window can be resized
  25: /// </summary>
  26: private bool CanResize
  27: {
  28:     get { return ((ResizeEnabled) && (resizeAnchor != ResizeAnchor.None)); }
  29: }

Then some code inside the mouse events:

MouseLeftButtonDown

   1: void window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
   2: {
   3:     if (CanResize)
   4:     {
   5:         // Capture the mouse
   6:         ((FrameworkElement)sender).CaptureMouse();
   7:         // Store the start position
   8:         this.initialResizePoint = e.GetPosition(this.Parent as UIElement);
   9:         initialWindowSize.Width = (!double.IsNaN(this.Width) ? this.Width : this.ActualWidth);
  10:         initialWindowSize.Height = (!double.IsNaN(this.Height) ? this.Height : this.ActualHeight);
  11:         this.initialWindowLocation.X = Canvas.GetLeft(this);
  12:         this.initialWindowLocation.Y = Canvas.GetTop(this);
  13:         // Set resizing to true
  14:         this.isResizing = true;
  15:     }
  16: }

when we push the left mouse button if we can resize, we save some initial values that will be used to compute the offsets needed to resize the window, here we get:

  • the starting position of the mouse
  • the initial window size (we look for the Width and Height value first, and if they aren’t set we look for the ActaulWidth and AactualHeight that will be computed if the size isn’t explicitly set by the user)
  • the initial window position

in the end we states that a resize operation is started.

MouseLeftButtonUp

   1: void window_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
   2: {
   3:     if ((ResizeEnabled) && (isResizing))
   4:     {
   5:         // Release the mouse
   6:         ((FrameworkElement)sender).ReleaseMouseCapture();
   7:         // Set resizing to false
   8:         isResizing = false;
   9:     }
  10: }

here we just release the mouse and end the resize operation, if there’s one in progress.

MouseMove

All the resizing logic goes here, so it’s a quite long routine, we divide it in 2 sections based on the current state of the control: if a resize operation isn’t started, we use this event to find if the mouse is near/inside an hotspot for resize (border or corner) and if so, to identify the correct hotspot; if the resizing operation is started (that is the mouse is inside an hotspot and the user pressed the mouse button), we use this event to compute the new window size and position.

   1: void window_MouseMove(object sender, MouseEventArgs e)
   2: {
   3:     if (ResizeEnabled)
   4:     {
   5:         Point pos = e.GetPosition(window);
   6:         if (!isResizing)
   7:         {
   8:             if ((pos.Y <= HotSpotWidth) && (pos.X <= HotSpotWidth))
   9:             {
  10:                 window.Cursor = Cursors.Hand;
  11:                 resizeAnchor = ResizeAnchor.TopLeft;
  12:             }
  13:             else if ((pos.Y <= HotSpotWidth) && (pos.X >= (window.ActualWidth - HotSpotWidth)))
  14:             {
  15:                 window.Cursor = Cursors.Hand;
  16:                 resizeAnchor = ResizeAnchor.TopRight;
  17:             }
  18:             else if (pos.Y <= HotSpotWidth)
  19:             {
  20:                 ...
  21:             }
  22:         }
  23:         else
  24:         {
  25:             ...resize the window...
  26:         }
  27:  
  28:             //let's resize the contentpresenter to fix the resize bug of controls inside a scrollviewer with
  29:             //horizontal scrollbar visible
  30:             contentpresenter.Width = this.Width - innerContentPresenterOffset;
  31:         }
  32:     }
  33: }

Here we get the current mouse position, then we check which hotspot the user hits, so we can change the cursor and set the internal variable that will indicate which action the user is going to take when he will press the left mouse button to start the resize operation.

   1: void window_MouseMove(object sender, MouseEventArgs e)
   2: {
   3:     if (ResizeEnabled)
   4:     {
   5:         Point pos = e.GetPosition(window);
   6:         if (!isResizing)
   7:         {
   8:             ...decide the resize action...
   9:         }
  10:         else
  11:         {
  12:             Point position = e.GetPosition(this.Parent as UIElement);
  13:  
  14:             double deltaX = position.X - initialResizePoint.X;
  15:             double deltaY = position.Y - initialResizePoint.Y;
  16:  
  17:             switch (resizeAnchor)
  18:             {
  19:                 case ResizeAnchor.Left:
  20:                     ResizeLeft(deltaX);
  21:                     break;
  22:                 ...
  23:                 case ResizeAnchor.TopLeft:
  24:                     ResizeLeft(deltaX);
  25:                     ResizeTop(deltaY);
  26:                     break;
  27:                 ...
  28:             }
  29:  
  30:             //let's resize the contentpresenter to fix the resize bug of controls inside a scrollviewer with
  31:             //horizontal scrollbar visible
  32:             contentpresenter.Width = this.Width - innerContentPresenterOffset;
  33:         }
  34:     }
  35: }

A resize operation is in progress, here we get the mouse position relative to the parent surface and we compute some delta values from the starting mouse position that will be used by the ResizeLeft(), ResizeTop(), ResizeRight() and ResizeBottom() functions to compute and assign the new window dimensions.

Implementing the autoscroll feature seemed simply at start, just wrap all inside a ScrollViewer (see the template posted at start) and create two dependency properties to show/hide the ScrollBars...then we encountered a nasty bug (see my previous post), the workaround for it is to explicitly set the content presenter size at start and during the resize operation (we need to take into account any offset that can originate from the template structure).

This is the line added to the MouseMove event:

   1: //let's resize the contentpresenter to fix the resize bug of controls inside a scrollviewer with
   2: //horizontal scrollbar visible
   3: contentpresenter.Width = this.Width - innerContentPresenterOffset;

The offset is computed during the window LayoutUpdated() event:

   1: void Window_LayoutUpdated(object sender, EventArgs e)
   2: {
   3:     // The layout is comepletely set, let's compute the content offset and fix its dimensions,
   4:     // we need to do this only the first time because it will cause reevaluation of the size of the 
   5:     // scrollviewer container too.
   6:     // This is the offset that takes into account any element of the window
   7:     if (innerContentPresenterOffset == -1)
   8:     {
   9:         // innerContentPresenterOffset = this.ActualWidth - contentpresenter.ActualWidth;
  10:         // We cannot use the starting content presenter size to compute the offset cause it can be greater than
  11:         // the actual window sie (due to the minvalue), so we compute it taking into account any horizontal offset
  12:         // of the scrollviewer
  13:         innerContentPresenterOffset = this.ActualWidth - (scrollcontent.ActualWidth - scrollcontent.Margin.Left
  14:             - scrollcontent.Margin.Right - scrollcontent.Padding.Left - scrollcontent.Padding.Right
  15:             - scrollcontent.BorderThickness.Left - scrollcontent.BorderThickness.Right);
  16:         innerContentPresenterOffset = Math.Max(innerContentPresenterOffset, 0);
  17:  
  18:         SetContentPresenterSizeAndMinSize();
  19:     }
  20: }

The full project is available at the end of the post.

As usual the control still has some limitations: it’s possible to resize a window to the outside of the Canvas of the Silverlight application loosing control on it; the window still misses some Maximize and Minimize buttons; it can also be good to have a sort of ‘action bar’ to track any window active and reposition it.

All these things will be introduced in some next release of this control, so stay tuned.

Example Solution:

Pervious posts:

Silverlight: Controls inside ScrollViewer - horizontal resize layout bug and related workaround

Silverlight: simulate a ‘Windows’ desktop application - part 2 (Dragging Window)

Silverlight: simulate a ‘Windows’ desktop application - part 1

Technorati Tags: ,,


Resizable, Resize, Silverlight, Window

10 comments

Related Post

  1. #1 da davides - Saturday March 2010 alle 01:15

    Very interesting job your Structura... however I tried the updated version of the window control but the error still occurs. I don't understand why... bye davides

  2. #2 da Talya - Saturday March 2010 alle 01:15

    Thank you very much, this was very helpfull :) Saved us a lot of time. Talya

  3. #3 da Jean François - Saturday March 2010 alle 01:15

    I want from Window add button or other on the Canvas. Please somebody help me with little sample code. Than you very much

  4. #4 da Jean François - Saturday March 2010 alle 01:15

    Resolu with : Canvas CV = (App.Current.RootVisual as ChatDeskTop).LayoutRoot; StackPanel SP = (StackPanel)CV.FindName("stackRight"); SP.Children.Add(btOpenTchat); Good job Guardian

  5. #5 da Guardian - Saturday March 2010 alle 01:15

    The file Generic.xaml that defines the standard template for each control must be placed inside a directory called 'Themes' and the build action must be set to 'Resource'. If you already have a Generic.xaml file in your projetc just merge the styles by copy/paste the file contents. as last note..if you copy/past the code and you modify the namespaces or class name..the template styles must be changed as well, check this line (and others similar)