Recently, I’ve been working a lot with Silverlight. I just finished writing a couple of controls that change the way that users interact with common data types. With the recent advent of rich interfaces hosted on the web, an improvement in the way that users can interact with data is taking place. So, I’m experimenting with a way to select ranges of information.
When dealing with ranges, a classic HTML site may have a couple of drop downs that have a beginning and ending number, text, or date. This interaction paradigm isn’t necessarily the best but it was the best that was available to us as developers. People got used to it but it doesn’t mean that they like it.
When a person fundamentally thinks of a range of somethings, they typically think horizontally or vertically. A couple of dropdowns is not how they think. If the range is a series of numeric values then the user wants to see the entire list of possible values and then have the values that he/she wants to select be highlighted. Visually, this removes the responsibility of the person to determine if a number is between start and end. They only have to make sure it’s highlighted.
Here’s an example:
As you can see, I’m selecting 14 through 20. Instantly, I know that 17 is in my range, I don’t have to look at two drop downs, compare them. I also don’t have the error prone possibility of having the end number be less than my beginning number. These types of paradigm shifts are going to happen now that we have richer user experience possibilities.
When I started creating this control, the first thing I wanted to do was create an expandable block. To do this, we’ve really got several controls.
We have a Canvas that contains a Grid that has an empty border, and another border that contains a Grid that contains a Rectangle.
XAML:
1: <Canvas x:Name="LayoutRoot" Background="Transparent">
2: <Grid Margin="0,2,0,2" MinWidth="20"
3: x:Name="window"
4: VerticalAlignment="Center" MouseMove="WindowMouseMove" MouseLeftButtonDown="WindowMouseDown" MouseLeftButtonUp="WindowMouseUp" >
5: <Border CornerRadius="4,4,4,4"
6: Background="Transparent"
7: BorderBrush="Blue" BorderThickness="1" />
8: <Border CornerRadius="2,2,2,2"
9: Background="Transparent" Padding="4,0,4,0">
10: <Grid x:Name="grd" Margin="3,0,3,0" MouseMove="BarMouseMove" MouseLeftButtonDown="BarMouseDown" MouseLeftButtonUp="BarMouseUp">
11: <Grid.RowDefinitions>
12: <RowDefinition Height="*" />
13: </Grid.RowDefinitions>
14: <Rectangle x:Name="rectangle" Opacity=".3" VerticalAlignment="Stretch">
15: <Rectangle.Fill>
16: <LinearGradientBrush EndPoint="1,1.2" StartPoint="1,0">
17: <GradientStop Color="LightBlue"/>
18: <GradientStop Color="Blue" Offset="1"/>
19: </LinearGradientBrush>
20: </Rectangle.Fill>
21: </Rectangle>
22: </Grid>
23: </Border>
24: </Grid>
25: </Canvas>
So that’s the layout. In the code, the major actions we’ve got to handle are the moving the selector bar and the resizing of the selector bar.
Let’s start with the resizing of the selector bar. there are three events that are important here. MouseDown, MouseUp, MouseMove.
When the mouse button goes down, we want to start resizing if the mouse is currently in the white space to the right or left of the rectangle.
1:
2: void WindowMouseDown(object sender, MouseButtonEventArgs e)
3: {
4: if (ResizeEnabled)
5: {
6: // Capture the mouse
7: ((FrameworkElement)sender).CaptureMouse();
8: // Store the start position
9: _initialResizePoint = e.GetPosition(Canvas);
10: _initialWindowSize.Width = window.ActualWidth;
11: _initialWindowLocation.X = Canvas.GetLeft(window);
12: // Set resizing to true
13: _isResizing = true;
14: }
15: }
Now, as the mouse moves (before the mouse button goes up), we want to resize to the left or right depending on the side the mouse is currently on.
1: void WindowMouseMove(object sender, MouseEventArgs e)
2: {
3: if (ResizeEnabled)
4: {
5: if (!_isResizing)
6: {
7: Point pos = e.GetPosition(window);
8: if (pos.X <= HotSpotWidth)
9: {
10: window.Cursor = Cursors.SizeWE;
11: _resizeAnchor = ResizeAnchor.Left;
12: }
13: else if (pos.X >= (window.ActualWidth - HotSpotWidth))
14: {
15: window.Cursor = Cursors.SizeWE;
16: _resizeAnchor = ResizeAnchor.Right;
17: }
18: else
19: {
20: window.Cursor = null;
21: _resizeAnchor = ResizeAnchor.None;
22: }
23: }
24: else
25: {
26: Point position = e.GetPosition(Canvas);
27: if (Canvas != null && position.X <= Canvas.ActualWidth && position.X >= 0)
28: {
29: double deltaX = position.X - _initialResizePoint.X;
30:
31: switch (_resizeAnchor)
32: {
33: case ResizeAnchor.Left:
34: ResizeLeft(deltaX);
35: break;
36: case ResizeAnchor.Right:
37: ResizeRight(deltaX);
38: break;
39: }
40: }
41: if (SelectionChanged != null)
42: {
43: SelectionChanged(Left, Right, _resizeAnchor == ResizeAnchor.Right ? Direction.Right : Direction.Left);
44: }
45: }
46: }
47: }
1:
2:
3: private void ResizeRight(double deltaX)
4: {
5: //Right = deltaX;
6: window.Width = Math.Min(Width - (_initialWindowLocation.X), Math.Max(_initialWindowSize.Width + deltaX, MinWindowWidth));
7: }
8:
9: private void ResizeLeft(double deltaX)
10: {
11: if ((_initialWindowSize.Width - deltaX) > MinWindowWidth)
12: {
13: // fix to avoid to move the window when we reached the minimal width resizing from the left
14: double maxX = _initialWindowLocation.X + (_initialWindowSize.Width - MinWindowWidth);
15: Canvas.SetLeft(window, Math.Min(_initialResizePoint.X + deltaX, maxX));
16: if (_initialResizePoint.X + deltaX < maxX)
17: {
18: window.Width = _initialWindowSize.Width - deltaX;
19: }
20: _initialWindowLocation.X = _initialResizePoint.X + deltaX;
21: }
22: }
Finally, when the mouse button goes up, we want to stop resizing the selector
1:
2: void WindowMouseUp(object sender, MouseButtonEventArgs e)
3: {
4: if ((ResizeEnabled) && (_isResizing))
5: {
6: // Release the mouse
7: ((FrameworkElement)sender).ReleaseMouseCapture();
8: // Set resizing to false
9: _isResizing = false;
10: if (SelectionFinishedChange != null)
11: {
12: SelectionFinishedChange(Left, Right, _resizeAnchor == ResizeAnchor.Left ? Direction.Left : Direction.Right);
13: }
14: }
15: }
This is the basic functionality. You can probably guess the implementation of the rest of the code but I’ll save you that effort and post the entire control as a silverlight project here.
You can expose the Left and Right (Width + Left) properties of this selector control to determine the selected positions on a higher level grid and thus determining the selected values of a range.
I hope that this saves you the trouble that I went through to figure out mouse capturing and resizing.