Introduction
This is a second part of creating a movable
usercontrol in WPF. I assume you have read the part I (even though that
was poorly written). I have made a lot of improvement to the file in
that article. In this part, you can download the latest base
MovabaleControl class and two controls that inherit it.
Background
In
HCI (human-computer interaction), we use Fitts' Law to measure the
performance of selecting a target. It is based on D (the distance
between the cursor and the selecting target), and W (the width of the
target), and it calculates the average time taken to complete the
selection.
Now, when study fall to a moving target selection, things are becoming more interesting.
The goal of this series is to build up a WPF application to perform a test on moving target selection.
We
already have built this base class, now we can create some different
moving targets, so we can find out if there is any technique we can use
to enhance the moving target selection performance.
Normal Target
The
first thing is, we need a really simple and base moving target, without
any technique (aid) that could facilitate the target selection. Well,
this is easy.
Suppose our base class has a namespace WPFTest.Targets and is called MovableControl.
In this NormalTarget xaml, we can define
1: <local:MovableUserControl x:Class="WPFTest.Targets.NormalTarget"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Height="40" Width="100" xmlns:local="clr-namespace:WPFTest.Targets">
5: <Grid>
6: <Rectangle Width="100" Height="40" Stroke="Black" Focusable="True" Name="TextRect" Fill="LightGray"></Rectangle>
7: <TextBlock Text="Normal Target" Margin="0" HorizontalAlignment="Center" VerticalAlignment="Center" Name="TargetText"></TextBlock>
8: </Grid>
9: </local:MovableUserControl>
The xmlns:local is a self-defined tag and it's pointing to the namespace we are after. Then we can reference our own UserControl.
Now in the code behind, we only need very simple code:
1: public partial class NormalTarget : MovableUserControl
2: {
3: public NormalTarget()
4: {
5: InitializeComponent();
6: }
7:
8: public override string Text
9: {
10: get { return TargetText.Text; }
11: set { TargetText.Text = value; TargetText.HorizontalAlignment = HorizontalAlignment.Center; }
12: }
13:
14: protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
15: {
16: base.OnMouseLeftButtonUp(e);
17: MouseClicked = true;
18: }
19:
20: public override void Start()
21: {
22: if (this.IsTarget)
23: {
24: this.TextRect.Fill = Brushes.Yellow;
25: }
26: base.Start();
27: }
28: }
In the Base class, I have two properties:
- MouseClicked -
indicates this target is clicked, because we want the inherited class
to tell us this is a valid mouse click, in case we have some special
technique handle the click differently
- IsTaget - indicates this is a target to be selected. It's used in a test case that there are multiple moving targets.
You can see from the code, I changed the control background color if it's a target.
ExpandedClick Technique
This
is a simple technique that I created might potentially enhance the
selection performance. If you ever used MacOS or had any experience in
HCI, you must have known there is something called "Expanded Target".
It's just like the MacOS task bar, when your mouse is approaching the
target, the target itself will grow bigger so that the user can easily
select it.
The difference between this "ExpandedClick" and
"Expanded Target" is, "ExpandedClick" control doesn't grow the size,
but expand the clickable area. It looks as follows:
The
yellow part is the control itself, the blue is the background, but the
white part between is actually clickable. So when the cursor is
approaching this target, it will expand the clickable area so that the
user can easily select it.
Let's look at the XAML code first:
1: <local:MovableUserControl x:Class="WPFTest.Targets.ExpandedClick"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Height="80" Width="160" xmlns:local="clr-namespace:WPFTest.Targets">
5: <Grid>
6: <Rectangle Width="160" Height="80" Fill="Transparent" Name="BackRect"></Rectangle>
7: <Rectangle Width="100" Height="40" Stroke="Black" Focusable="True" Name="TextRect" Fill="LightGray"></Rectangle>
8: <TextBlock Text="Expanded Target" Margin="38,32,40,32" Name="TargetText"></TextBlock>
9: </Grid>
10: </local:MovableUserControl>
It's very similar to the Normal target; however, it has one
more Rectangle that is the clickable area. By default, it's
transparent; when the mouse is hover, it will change the color.
The code behind will be like:
1: public partial class ExpandedClick : MovableUserControl
2: {
3: // these two values define the allowable offset of this control, because the control itself actually is larger than
4: // what the user can see due to the clickable area is expanded
5: private int horizontalOffset = 0;
6: private int verticalOffset = 0;
7:
8: public ExpandedClick()
9: {
10: InitializeComponent();
11: }
12:
13: private Brush m_ExpandedBackground = Brushes.Azure;
14: public Brush BackgroundBrush
15: {
16: get { return m_ExpandedBackground; }
17: set { m_ExpandedBackground = value; }
18: }
19:
20: public override string Text
21: {
22: get { return TargetText.Text; }
23: set { TargetText.Text = value; TargetText.TextAlignment = TextAlignment.Center; }
24: }
25:
26: protected override void OnMouseEnter(MouseEventArgs e)
27: {
28: BackRect.Fill = BackgroundBrush;
29: base.OnMouseEnter(e);
30: }
31:
32: protected override void OnMouseLeave(MouseEventArgs e)
33: {
34: BackRect.Fill = Brushes.Transparent;
35: base.OnMouseLeave(e);
36: }
37:
38: protected override void OnMouseDown(MouseButtonEventArgs e)
39: {
40: BackRect.Fill = Brushes.Transparent;
41: base.OnMouseDown(e);
42: }
43:
44: protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
45: {
46: base.OnMouseLeftButtonUp(e);
47: BackRect.Fill = BackgroundBrush;
48: this.TextRect.Fill = Brushes.LightGray;
49: MouseClicked = true;
50: }
51:
52: public override void Start()
53: {
54: if (this.IsTarget)
55: {
56: TextRect.Fill = Brushes.Yellow;
57: }
58:
59: base.Start();
60: }
61:
62: private void precal()
63: {
64: if (horizontalOffset == 0 && verticalOffset == 0)
65: {
66: // do the pre-calculation in here
67: horizontalOffset = (int)(this.Width - TextRect.Width) / 2;
68: verticalOffset = (int)(this.Height - TextRect.Height) / 2;
69: }
70: }
71:
72: public override int LeftOffset
73: {
74: get
75: {
76: precal();
77: return -horizontalOffset;
78: }
79: }
80:
81: public override int TopOffset
82: {
83: get
84: {
85: precal();
86: return -verticalOffset;
87: }
88: }
89:
90: public override int RightOffset
91: {
92: get
93: {
94: precal();
95: return horizontalOffset;
96: }
97: }
98:
99: public override int BottomOffset
100: {
101: get
102: {
103: precal();
104: return verticalOffset;
105: }
106: }
107: }
It's a little more compared to the normal target. First, in the base control, I defined four properties:
- LeftOffset
- TopOffset
- BottomOffset
- RightOffset
They
are used to detect if the control hits the boundary. If yes, we need to
re-calculate where the control is heading in the next interval. The
detection code is like:
1: if (currentX + this.ActualWidth >= parentWidth + RightOffset ||
2: currentY + this.ActualHeight >= parentHeight + BottomOffset ||
3: currentX <= LeftOffset ||
4: currentY <= TopOffset)
5: {
6: restart = true;
7: }
In this control, we don't want the user see such a large
"block" on the screen, because the actual visual target part doesn't
include the clickable area. Hence, we want to tell the parent, here is
a small offset you can go, don't bounce yet.You can download the sample files here:
MovableTargets.zip (5.21 kb)