Ok, I think I could devote too much time to it, but it sounded like a cool task: P
I created a Decorator class called 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 should be used in XAML around the control you want to focus on. It has three properties: IsOpen
, TipText
and HasBeenShown
. IsOpen
must be set to true
to display the focus and tip window (and automatically set to false
when the prompt window is closed). TipText
allows you to specify the text that should be displayed in the tooltip window. And HasBeenShown
keeps track of whether the tooltip has been shown, so it only appears once. You can use Bindings for all of these properties or set them from code.
To create a focus effect, this class uses another custom Adorner, 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 because it actually uses screen capture) the entire window, keeping the desired controls focused and clear (and moving - that is, in TextBoxes, the text input carriage will still be visible and blinking).
To use this Decorator, you must install it the same way 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 end result when ShowLoginTip
set to true
:

KNOWN ISSUES
Currently, a simple MessageBox
used to display the tooltip, but you can create your own Window
class for tooltips, label it whatever you want, and call it ShowDialog()
instead of MessageBox.Show()
(and you can also control where Window
appears if you you want it to appear right next to the focused control or something like that).
Also, this will not work in UserControls right away because AdornerLayer.GetAdornerLayer(AdornedElement)
will return null
inside UserControls. This could be easily AdornerLayer
looking for the AdornerLayer
UserControl
(or parent of the parent, recursively). There are functions for this.
This will not work for pages, but only for Windows. Just because I use Window.GetWindow(this)
to get the parent Window
Decorator ... You can use other functions to get a parent that could work either on Windows, or on pages, or whatever. As with the AdornerLayer
problem, there are many solutions to this.
In addition, I assume that this can be animated in some way (for example, the blur and dimming effect appears, for example), but did not really look at it ...