When a user is running my program for the first time, I want them to go through a series of tips. Each time they hit a certain \"checkpoint\", the program will pause what it
Ok, I think I may have dedicated too much time to this, but it sounded like a cool challenge :P
I've created a Decorator class named TipFocusDecorator
that handles all this.
public class TipFocusDecorator : Decorator
{
public bool IsOpen
{
get { return (bool)GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
// Using a DependencyProperty as the backing store for Open. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register("IsOpen", typeof(bool), typeof(TipFocusDecorator),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, IsOpenPropertyChanged));
public string TipText
{
get { return (string)GetValue(TipTextProperty); }
set { SetValue(TipTextProperty, value); }
}
// Using a DependencyProperty as the backing store for TipText. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TipTextProperty =
DependencyProperty.Register("TipText", typeof(string), typeof(TipFocusDecorator), new UIPropertyMetadata(string.Empty));
public bool HasBeenShown
{
get { return (bool)GetValue(HasBeenShownProperty); }
set { SetValue(HasBeenShownProperty, value); }
}
// Using a DependencyProperty as the backing store for HasBeenShown. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HasBeenShownProperty =
DependencyProperty.Register("HasBeenShown", typeof(bool), typeof(TipFocusDecorator), new UIPropertyMetadata(false));
private static void IsOpenPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var decorator = sender as TipFocusDecorator;
if ((bool)e.NewValue)
{
if (!decorator.HasBeenShown)
decorator.HasBeenShown = true;
decorator.Open();
}
if (!(bool)e.NewValue)
{
decorator.Close();
}
}
TipFocusAdorner adorner;
protected void Open()
{
adorner = new TipFocusAdorner(this.Child);
var adornerLayer = AdornerLayer.GetAdornerLayer(this.Child);
adornerLayer.Add(adorner);
MessageBox.Show(TipText); // Change for your custom tip Window
IsOpen = false;
}
protected void Close()
{
var adornerLayer = AdornerLayer.GetAdornerLayer(this.Child);
adornerLayer.Remove(adorner);
adorner = null;
}
}
This Decorator must be used in XAML around the control you want to focus. It has three properties: IsOpen
, TipText
and HasBeenShown
. IsOpen
must be set to true
to make the focus and tip window appear (and is set to false
automatically when the tip window is closed). TipText
allows you to define the text that must be shown in the tip window. And HasBeenShown
keeps track of whether the tip window has been shown, so it only shows once. You can use Bindings for all these properties or set them from code-behind.
To create the focus effect, this class uses another custom Adorner, the TipFocusAdorner
:
public class TipFocusAdorner : Adorner
{
public TipFocusAdorner(UIElement adornedElement)
: base(adornedElement)
{
}
protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var root = Window.GetWindow(this);
var adornerLayer = AdornerLayer.GetAdornerLayer(AdornedElement);
var presentationSource = PresentationSource.FromVisual(adornerLayer);
Matrix transformToDevice = presentationSource.CompositionTarget.TransformToDevice;
var sizeInPixels = transformToDevice.Transform((Vector)adornerLayer.RenderSize);
RenderTargetBitmap rtb = new RenderTargetBitmap((int)(sizeInPixels.X), (int)(sizeInPixels.Y), 96, 96, PixelFormats.Default);
var oldEffect = root.Effect;
var oldVisibility = AdornedElement.Visibility;
root.Effect = new BlurEffect();
AdornedElement.SetCurrentValue(FrameworkElement.VisibilityProperty, Visibility.Hidden);
rtb.Render(root);
AdornedElement.SetCurrentValue(FrameworkElement.VisibilityProperty, oldVisibility);
root.Effect = oldEffect;
drawingContext.DrawImage(rtb, adornerLayer.TransformToVisual(AdornedElement).TransformBounds(new Rect(adornerLayer.RenderSize)));
drawingContext.DrawRectangle(new SolidColorBrush(Color.FromArgb(22, 0, 0, 0)), null, adornerLayer.TransformToVisual(AdornedElement).TransformBounds(new Rect(adornerLayer.RenderSize)));
drawingContext.DrawRectangle(new VisualBrush(AdornedElement) { AlignmentX = AlignmentX.Left, TileMode = TileMode.None, Stretch = Stretch.None },
null,
AdornedElement.RenderTransform.TransformBounds(new Rect(AdornedElement.RenderSize)));
}
}
This dims and blurs (and freezes, since it actually uses a screen capture) all the window, while keeping the desired controls focused and clear (and moving - i.e. in TextBoxes, the text input caret will still be visible and blinking).
To use this Decorator, you only must set it like this in XAML:
<StackPanel>
<local:TipFocusDecorator x:Name="LoginDecorator"
TipText="Enter your username and password and click 'Login'"
IsOpen="{Binding ShowLoginTip}">
<local:LoginForm />
</local:TipFocusDecorator>
</StackPanel>
And the final result, when ShowLoginTip
is set to true
:
KNOWN ISSUES
Right now this uses a simple MessageBox
to show the tip, but you can create your own Window
class for the tips, style it as you want, and call it with ShowDialog()
instead of the MessageBox.Show()
(and you could also control where the Window
appears, if you want it to appear right next to the focused Control or something like that).
Also, this won't work inside UserControls right away, because AdornerLayer.GetAdornerLayer(AdornedElement)
will return null
inside UserControls. This could be easily fixed by looking for the AdornerLayer
of the PARENT of the UserControl
(or the parent of the parent, recursively). There are functions around to do so.
This won't work for Pages either, only for Windows. Simply because I use Window.GetWindow(this)
to get the parent Window
of the Decorator... You could use other functions to get the parent, that could work either with Windows, Pages or whatever. As with the AdornerLayer
problem, there are plenty of solutions for this around here.
Also, I guess this could be animated somehow (making the blur and dim effect appear gradually, for instance), but haven't really looked into it...
You can create your tip as a window and show it using ShowDialog()
. This gives you a Modal dialog, as others have suggested. Be sure to set it's owner. Just before you show it, you can use
<UIElement.Effect>
<BlurEffect/>
</UIelement.Effect>
to set your window or outer container's(grid maybe) Blur Effect. The radius property sets the "level" of blur, so I imagine you can set it to 0 initially and modify it programatically when you show your dialog