Implement mouse interaction on custom control based on FrameworkElement - wpf

This is just example to learn from.
I want create custom control with completely different looks. Hence, according to https://learn.microsoft.com/en-us/dotnet/framework/wpf/controls/control-authoring-overview, I derive from FrameworkElement
and override OnRender method, also OverriderMesure and ArrangeOverride if needed.
Now I want implement mouse interaction, for example: on hover change color from red to blue. How I should do it?
public class Box : FrameworkElement
{
private static Color defaultColor = Colors.Red;
public static DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(SolidColorBrush), typeof(Box),
new FrameworkPropertyMetadata(new SolidColorBrush(defaultColor), FrameworkPropertyMetadataOptions.AffectsRender));
public SolidColorBrush Color
{
get { return (SolidColorBrush)GetValue(ColorProperty); }
set { SetValue(ColorProperty, value); }
}
static Box()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Box), new FrameworkPropertyMetadata(typeof(Box)));
}
protected override void OnRender(DrawingContext drawingContext)
{
// It's just example, I know shape is wayyy too simple to involve custom render.
drawingContext.DrawRectangle(Color, null, new Rect(0, 0, ActualWidth, ActualWidth));
}
protected override void OnMouseEnter(MouseEventArgs e)
{
Color = new SolidColorBrush(Colors.Blue); // Set to color
}
protected override void OnMouseLeave(MouseEventArgs e)
{
Color = new SolidColorBrush(defaultColor); // Back to default
}
protected override Size MeasureOverride(Size constraint)
{
...
}
protected override Size ArrangeOverride(Size finalSize)
{
...
}
}
So far I deduce:
Normally, if I had derive from Control, I would have use VSM for this. Unfortunately VSM isn't available until ControlTemplate hierarchy tree, so controls which have Template property. So if I decided to draw my control by myself I need use for this Routed Events, in this particular example OnMouseEnter(MouseEventArgs), OnMouseLeave(MouseEventArgs) and some dependency property, like code above.
It this right approach? Please remember it's for learning purpose so FrameworkElement as base is obligatory.
I can see some drawbacks, becouse If we want control onHover color (in code above is harcoded to blue) I need mess around with code behind, or create another dependency property for this.

Unfortunately VSM isn't available until ControlTemplate hierarchy tree, so controls which have Template property.
This it not true.
You can use VSM normally, with some small changes. Read: https://msdn.microsoft.com/en-us/library/system.windows.visualstatemanager(v=vs.110).aspx#Examples. Take a closer look on example.

Related

Custom WPF canvas with multiple underlying visuals

Found interesting article describing how to create custom canvas control by exposing methods Add and Remove for underlying visuals of the Panel class. This way I could create universal canvas that can accommodate absolutely any way to draw on it, GDI+, Canvas, Drawings, etc. For example, the first layer would be Bitmap, second Canvas, third DrawingVisual, etc.
For simplicity, I'd like to extend existing Canvas, so I could have original behavior provided by default Canvas control + could create as many additional Visuals as I want to.
Here is what I have now.
public class VisualCanvas : Canvas
{
protected IList<Visual> _visuals = null;
protected override int VisualChildrenCount => _visuals.Count;
protected override Visual GetVisualChild(int index) => _visuals.ElementAtOrDefault(index);
public VisualCanvas()
{
_visuals = new List<Visual>();
_visuals.Add(new DrawingVisual());
//(_visuals[0] as DrawingVisual).RenderOpen();
}
public void AddVisual(Visual visual)
{
_visuals.Add(visual);
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
public void DeleteVisual(Visual visual)
{
_visuals.Remove(visual);
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
}
Unfortunately, DrawingVisual that I add in the constructor doesn't make this control to act like original Canvas because 2 methods that I overridden seem to expect different kind of Visual, not DrawingVisual.
How do I make this control work like original Canvas?
Found similar question on MSDN. Appears to be it's not enough to override only 2 methods to keep all children in one list of visuals. Default canvas collection InternalChildren also needs to be taken into account.
This implementation seems to keep default canvas behavior + new visuals, but may have unpredictable behavior because 2 class collections share the same index. Looks like it would be easier to extend this class with new properties for each visual type rather than trying to override existing ones. Feel free to post better ideas and implementations.
public class VisualCanvas : Canvas
{
protected IList<Visual> _visuals = new List<Visual>();
protected override int VisualChildrenCount => _visuals.Count + InternalChildren.Count;
protected override Visual GetVisualChild(int index) => _visuals.ElementAtOrDefault(index) ?? InternalChildren[index - _visuals.Count];
public void AddVisual(Visual visual)
{
_visuals.Add(visual);
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
public void DeleteVisual(Visual visual)
{
_visuals.Remove(visual);
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
}

How to recognize WPF drawing visuals in UIAutomation?

Our application has a canvas, to which we add the drawing visuals (like lines, polygons etc)
// sample code
var canvas = new Canvas(); // create canvas
var visuals = new VisualCollection(canvas); // link the canvas to the visual collection
visuals.Add(new DrawingVisual()); // add the visuals to the canvas
visuals.Add(new DrawingVisual());
Our goal is to add these visuals to the canvas via automation and validate that they are properly added. We use a framework that is based on Microsoft's UIAutomation.
When using a tool like "Inspect" to inspect the visual structure, I couldnt locate the canvas. Did some research and figured out that you need to override the OnCreateAutomationPeer method from UIElement, and return applicable AutomationPeer object to be able to be able to see that in automation.
Made the change and now I can see the canvas, however I cant still see any of the visuals added under the canvas.
Can anyone help me understand what the issue is?
Things tried / alternatives:
Tried to employ the OnCreateAutomationPeer technique, but the
DrawingVisuals dont derive from UIElement, and I cant add UIElements
to Canvas.VisualCollection.
Image recognition is an option, but we
are trying to avoid it for performance/maintenance considerations.
Only UIElement can be seen from UI Automation (like you have seen, OnCreateAutomationPeer starts from this class, not from the Visual class).
So you need to add UIElement (or derived like FrameworkElement) to the canvas, if you want it to be usable by UIAutomation.
You can create your own class like described here: Using DrawingVisual Objects or with a custom UserControl or use an existing one that suits your need but it must derive from UIElement somehow.
Once you have a good class, you can use the default AutomationPeer or override the method and adapt more closely.
If you want to keep Visual objects, one solution is to modify the containing object (but it still needs to derive from UIElement). For example, here if I follow the article in the link, I can write a custom containing object (instead of a canvas of your sample code so you may have to adapt slightly) like this:
public class MyVisualHost : UIElement
{
public MyVisualHost()
{
Children = new VisualCollection(this);
}
public VisualCollection Children { get; private set; }
public void AddChild(Visual visual)
{
Children.Add(visual);
}
protected override int VisualChildrenCount
{
get { return Children.Count; }
}
protected override Visual GetVisualChild(int index)
{
return Children[index];
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new MyVisualHostPeer(this);
}
// create a custom AutomationPeer for the container
private class MyVisualHostPeer : UIElementAutomationPeer
{
public MyVisualHostPeer(MyVisualHost owner)
: base(owner)
{
}
public new MyVisualHost Owner
{
get
{
return (MyVisualHost)base.Owner;
}
}
// a listening client (like UISpy is requesting a list of children)
protected override List<AutomationPeer> GetChildrenCore()
{
List<AutomationPeer> list = new List<AutomationPeer>();
foreach (Visual visual in Owner.Children)
{
list.Add(new MyVisualPeer(visual));
}
return list;
}
}
// create a custom AutomationPeer for the visuals
private class MyVisualPeer : AutomationPeer
{
public MyVisualPeer(Visual visual)
{
}
// here you'll need to implement the abstrat class the way you want
}
}

WPF Control moves but its Adorner - Not :"/

I created an adorner on a WPF line element, because there was neet to add some text.
Now, when this line is moved, the adorner does not "follow" the line automatically. In fact, it does not refresh itsef:
here black curves is the Control drawing, and the red "120 m" is the adorner one.
Some code
void SegmentLine_Loaded(object sender, RoutedEventArgs e)
{
AdornerLayer aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
{
aLayer.Add(new TextAdorner(this));
}
}
class TextAdorner : Adorner
{
public TextAdorner(UIElement adornedElement)
: base(adornedElement)
{
}
protected override void OnRender(DrawingContext drawingContext)
{
SegmentLine segment = (this.AdornedElement as SegmentLine);
if (segment != null)
{
Rect segmentBounds = new Rect(segment.DesiredSize);
var midPoint = new Point(
(segment.X1 + segment.X2) / 2.0,
(segment.Y1 + segment.Y2) / 2.0);
var lineFont = // get line font as Font
FormattedText ft = new FormattedText(
string.Format("{0} m", segment.Distance),
Thread.CurrentThread.CurrentCulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface(lineFont.FontFamily.ToString()),
ligneFont.Size, Brushes.Red);
drawingContext.DrawText(ft, midPoint);
}
}
}
Why MeasureOverride, etc aren't being called
Your adorner's MeasureOverride, ArrangeOverride, and OnRender aren't being called because your SegmentLine control is never changing size or position:
Since your SegmentLine doesn't implement MeasureOverride, it always has the default size assigned by the layout engine.
Since your SegmentLine doesn't implement ArrangeOverride or manipulate any transforms, its position is always exactly the upper-left corner of the container.
The Adorner's MeasureOverride, ArrangeOverride and OnRender are only called by WPF under these conditions:
The AdornedElement changes size or position (this the most common case), or
One of the Adorner's properties chagnes and that property is marked AffectsMeasure, AffectsArrange, or AffectsRender, or
You call InvalidateMeasure(), InvalidateArrange(), or InvalidateVisuaul() on the adorner.
Because your SegmentLine never changes size or position, case 1 doesn't apply. Since you don't have any such properties on the Adorner and don't call InvalidateMeasure(), InvalidateArrange() or InvalidateVisual(), the other cases don't apply either.
Precise rules for Adorner re-measure
Here are the precise rules for when an adorned element change triggers a call to Adorner.MeasureOverride:
The adorned element must force a layout pass by invalidating its Measure or Arrange in response to some event. This could be triggered automatically by a change to a DependencyProperty with AffectsMeasure or AffectsArrange, or by a direct call to InvalidateMeasure(), InvalidateArrange() or InvalidateVisual().
The adorned element's Measure and Arrange methods must not be called directly from user code between the invalidation and the layout pass. In other words, you must wait for the layout manager to do the job.
The adorned element must make a non-trivial change to either its RenderSize or its Transform.
The combination of all transforms between the AdornerLayer and the adorned element must be affine. This will generally be the case as long as you are not using 3D.
Your SegmentLine is just drawing the line in a new place rather than updating its own dimensions, thereby omitting my requirement #3 above.
Recommendation
Normally I would recommend your adorner have AffectsRender DependencyProperties bound to the SegmentLine's properties, so any time X1, Y1, etc change in the SegmentLine they are also updated in the Adorner which causes the Adorner to re-render. This provides a very clean interface, since the adorner can be used on any control that has properties X1, Y1, etc, but it is less efficient than tightly coupling them.
In your case the adorner is clearly tightly bound to your SegmentLine, so I think it makes just as much sense to call InvalidateVisual() on the adorner from the SegmentLine's OnRender(), like this:
public class SegmentLine : Shape
{
TextAdorner adorner;
...
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if(adorner==null)
{
var layer = AdornerLayer.GetAdornerLayer(this); if(layer==null) return;
adorner = new TextAdorner(this);
... set other adorner properties and events ...
layer.Add(adorner);
}
adorner.InvalidateVisual();
}
}
Note that this doesn't deal with the situation where the SegmentLine is removed from the visual tree and then added again later. Your original code doesn't deal with this either, so I avoided the complexity of dealing with that case. If you need that to work, do this instead:
public class SegmentLine : Shape
{
AdornerLayer lastLayer;
TextAdorner adorner;
...
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var layer = AdornerLayer.GetAdornerLayer(this);
if(layer!=lastLayer)
{
if(adorner==null)
{
adorner = new TextAdorner(this);
... set other adorner properties and events ...
}
if(lastLayer!=null) lastLayer.Remove(adorner);
if(layer!=null) layer.Add(adorner);
lastLayer = layer;
}
adorner.InvalidateVisual();
}
}
How is the line being moved? Does the MeasureOverride or ArrangeOverride of the adorner get invoked after the move? OnRender will only get invoked if the visual is invalidated (e.g. invalidatevisual) so I'm guessing that the render isn't being invalidated.
May be you wanted to use segmentBounds to define midPoint? Otherwise what is it doing there? Looks like you are defining midPoint relative to not rerendered segment.
idiot fix, but it works
AdornerLayer aLayer;
void SegmentLine_Loaded(object sender, RoutedEventArgs e)
{
aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
{
aLayer.Add(new TextAdorner(this));
}
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (aLayer != null)
{
aLayer.Update();
}
}
Now, the problem is that when I click on a the adorner the control itself does not recieve the hit...

wpf converter : setting multiple properties

I am used to using converters that return a value per property, such as Foreground color.
Is it possible to have a converter that works with multiple properties?
such as: Foreground, Background, Font-Weight, Font-Size
How can I create one converter (or less than 4) that could set multiple properties?
No, converters aren't designed for that. You could possibly go down the attached behaviour route and set the properties, based on a bound dependency property (I assume) on attach?
Edit: behaviours are part of the Blend SDK, the basic structure of what you want is:
public class MyBehavior : Behavior<TextBlock>
{
//// <-- Dependency property here
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.Foreground = CalculateForeground();
this.AssociatedObject.Background = CalculateBackground();
// etc..
}
private Brush CalculateForeground()
{
// Do some calculations based on the dependency property
}
private Brush CalculateBackground()
{
// Do some calculations based on the dependency property
}
protected override void OnDetaching()
{
base.OnDetaching();
// You might want to reset to default here, or just do nothing
}
}

WPF Listview modification

For a very specific reason I want to select ListViewItems on mouse button up, not actually on mouse button down. I want this behaviour to be embedded in the control. Is it possible to achieve this? can anyone give hint?
Yes it's definitely possible using attached properties. Define an attached property called SelectOnMouseUp and when it's set to true, hook to your ItemsContainerGenerator events to discover when a new item container is added. Then when you get an event for a new item container, hook into its PreviewMouseDown and ignore it (set e.Handled to true), and hook into its MouseUp event and perform the selection (set IsSelected to true).
Aviad P.'s answer is a good one and a clever use of attached properties, but I tend to use a different technique most of the time:
Subclass ListViewItem.
Override OnMouseLeftButtonDown and OnMouseRightButton to do nothing.
Override OnMouseLeftButtonUp / OnMouseRightButtonUp to call base.OnMouseLeftButtonDown / base.OnMouseRightButtonDown.
Subclass ListView.
Override GetContainerForItemOverride() to return your ListViewItem override
This seems easier to me than subscribing to ItemContainer events and adding handlers dynamically.
This is what it looks like:
public class MouseUpListViewItem : ListViewItem
{
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {}
protected override void OnMouseRightButtonDown(MouseButtonEventArgs e) {}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
}
protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
{
base.OnMouseRightButtonDown(e);
}
}
public class MouseUpListView : ListView
{
protected override DependencyObject GetContainerForItemOverride()
{
return new MouseUpListViewItem();
}
}
I like this technique because there is less code involved.

Resources