Access subclass of FrameworkElement from VisualTreeHelper.HitTest - c#

I have a custom element class that is a subclass of FrameworkElement.
public class MyCustomElement : FrameworkElement
{
private VisualCollection children;
public MyCustomElement()
{
this.children = new VisualCollection(this);
this.children.Add(MyDrawingRoutines());
}
private DrawingVisual MyDrawingRoutines()
{
//...
}
protected override int VisualChildrenCount
{
get { return children.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= children.Count)
{
throw new ArgumentOutOfRangeException();
}
return children[index];
}
}
The UI holds a canvas in which these custom drawing elements are added and hit testing is performed.
public partial class MainWindow : Window
{
private MyCustomElement myCustomElement;
public MainWindow()
{
InitializeComponent();
myCustomElement = new MyCustomElement();
myCanvas.Children.Add(myCustomElement);
}
private void myCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
System.Windows.Point pt = e.GetPosition((UIElement)sender);
VisualTreeHelper.HitTest(this, null, new HitTestResultCallback(leftClickCallback), new PointHitTestParameters(pt));
}
public HitTestResultBehavior leftClickCallback(HitTestResult result)
{
if (result.VisualHit.GetType() == typeof(DrawingVisual))
{
if (((DrawingVisual)result.VisualHit).Opacity == 1.0)
{
((DrawingVisual)result.VisualHit).Opacity = 0.4;
}
else
{
((DrawingVisual)result.VisualHit).Opacity = 1.0;
}
}
return HitTestResultBehavior.Stop;
}
}
}
This code works as expected, but I cannot find a way to determine which MyCustomElement the detected DrawingVisual belongs. Right now, the opacity adjustment is done only superficially correct? I would like to change the opacity property on MyCustomElement, have the MyDrawingRoutines() method apply it, and have only the finished DrawingVisual drawn on the Canvas.

You should be able to cast the Parent property of the DrawingVisual to your MyCustomElement class:
public HitTestResultBehavior leftClickCallback(HitTestResult result)
{
var visual = result.VisualHit as DrawingVisual;
if (visual != null)
{
var element = visual.Parent as MyCustomElement;
if (element != null)
{
if (element.Opacity == 1.0)
{
element.Opacity = 0.4;
}
else
{
element.Opacity = 1.0;
}
}
}
return HitTestResultBehavior.Stop;
}
In case you need to get the parent of any visual (not just a ContainerVisual, which has the Parent property as shown above), you may use VisualTreeHelper.GetParent:
var visual = result.VisualHit;
var element = VisualTreeHelper.GetParent(visual) as MyCustomElement;

Related

How to set Xamarin.Forms Elements BindableProperties from a Custom Renderer?

I have been trying to set a bindable property value in my Element from my native control through a custom renderer. My native control is a view (painview) where you can draw and I am trying to get the drawing and set it, as a base64 string, to a bindable property Signature in my Element.
This is my Native Control
public class PaintView : View
{
Canvas _drawCanvas;
Bitmap _canvasBitmap;
readonly Paint _paint;
readonly Dictionary<int, MotionEvent.PointerCoords> _coords = new Dictionary<int, MotionEvent.PointerCoords>();
public Bitmap CanvasBitmap { get => _canvasBitmap; private set => _canvasBitmap = value; }
private readonly string TAG = nameof(PaintView);
public event EventHandler OnLineDrawn;
public PaintView(Context context) : base(context, null, 0)
{
_paint = new Paint() { Color = Color.Blue, StrokeWidth = 5f, AntiAlias = true };
_paint.SetStyle(Paint.Style.Stroke);
}
public PaintView(Context context, IAttributeSet attrs) : base(context, attrs) { }
public PaintView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) { }
protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
{
base.OnSizeChanged(w, h, oldw, oldh);
_canvasBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); // full-screen bitmap
_drawCanvas = new Canvas(_canvasBitmap); // the canvas will draw into the bitmap
}
public override bool OnTouchEvent(MotionEvent e)
{
switch (e.ActionMasked)
{
case MotionEventActions.Down:
{
int id = e.GetPointerId(0);
var start = new MotionEvent.PointerCoords();
e.GetPointerCoords(id, start);
_coords.Add(id, start);
return true;
}
case MotionEventActions.PointerDown:
{
int id = e.GetPointerId(e.ActionIndex);
var start = new MotionEvent.PointerCoords();
e.GetPointerCoords(id, start);
_coords.Add(id, start);
return true;
}
case MotionEventActions.Move:
{
for (int index = 0; index < e.PointerCount; index++)
{
var id = e.GetPointerId(index);
float x = e.GetX(index);
float y = e.GetY(index);
_drawCanvas.DrawLine(_coords[id].X, _coords[id].Y, x, y, _paint);
_coords[id].X = x;
_coords[id].Y = y;
OnLineDrawn?.Invoke(this, EventArgs.Empty);
}
Invalidate();
return true;
}
case MotionEventActions.PointerUp:
{
int id = e.GetPointerId(e.ActionIndex);
_coords.Remove(id);
return true;
}
case MotionEventActions.Up:
{
int id = e.GetPointerId(0);
_coords.Remove(id);
return true;
}
default:
return false;
}
}
protected override void OnDraw(Canvas canvas)
{
// Copy the off-screen canvas data onto the View from it's associated Bitmap (which stores the actual drawn data)
canvas.DrawBitmap(_canvasBitmap, 0, 0, null);
}
public void Clear()
{
_drawCanvas.DrawColor(Color.Black, PorterDuff.Mode.Clear); // Paint the off-screen buffer black
Invalidate(); // Call Invalidate to redraw the view
}
public void SetInkColor(Color color)
{
_paint.Color = color;
}
}
The property PaintView._canvasBitmap is the one I want to be set in my Xamarin.Form Element through my custom renderer.
This is my Custom Renderer
public class SketchViewRenderer : ViewRenderer<SketchView, PaintView>
{
public SketchViewRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<SketchView> e)
{
if (Control == null)
{
var paintView = new PaintView(Context);
paintView.SetInkColor(Element.InkColor.ToAndroid());
SetNativeControl(new PaintView(Context));
MessagingCenter.Subscribe<SketchView>(this, nameof(SketchView.OnClear), OnMessageClear);
Control.OnLineDrawn += PaintViewLineDrawn;
}
}
private void PaintViewLineDrawn(object sender, EventArgs e)
{
var sketchCrl = (ISketchViewController)Element;
if (sketchCrl == null) return;
try
{
Element.SetValueFromRenderer(SketchView.SignatureProperty, Utils.Utils.BitmapToBase64(Control.CanvasBitmap));
sketchCrl.SendSketchUpdated(Utils.Utils.BitmapToBase64(Control.CanvasBitmap));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == SketchView.InkColorProperty.PropertyName)
{
Control.SetInkColor(Element.InkColor.ToAndroid());
}
if (e.PropertyName == SketchView.ClearProperty.PropertyName)
{
if (Element.Clear) OnMessageClear(Element);
}
}
private void OnMessageClear(SketchView sender)
{
if (sender == Element) Control.Clear();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
MessagingCenter.Unsubscribe<SketchView>(this, nameof(SketchView.OnClear));
Control.OnLineDrawn -= PaintViewLineDrawn;
}
base.Dispose(disposing);
}
}
I have tried changing my Element.Signature property through the SketchViewRenderer.PaintViewLineDrawn(...) method without success. This has been prove when debugging my view model where the property has not been set as expected.
My Xamarin.Forms Element looks as follow
public class SketchView : View, IDoubleTappedController, ISketchViewController
{
public static readonly BindableProperty SignatureProperty = BindableProperty.Create(nameof(Signature), typeof(string), typeof(SketchView), null, defaultBindingMode: BindingMode.TwoWay);
public string Signature
{
get => (string)GetValue(SignatureProperty);
set => SetValue(SignatureProperty, value);
}
public static readonly BindableProperty MultiTouchEnabledProperty = BindableProperty.Create(nameof(MultiTouchEnabled), typeof(bool), typeof(SketchView), false);
public bool MultiTouchEnabled
{
get => (bool)GetValue(MultiTouchEnabledProperty);
set => SetValue(MultiTouchEnabledProperty, value);
}
public static readonly BindableProperty InkColorProperty = BindableProperty.Create(nameof(InkColor), typeof(Xamarin.Forms.Color), typeof(SketchView), Xamarin.Forms.Color.Azure);
public Xamarin.Forms.Color InkColor
{
get => (Xamarin.Forms.Color)GetValue(InkColorProperty);
set => SetValue(InkColorProperty, value);
}
public static readonly BindableProperty ClearProperty = BindableProperty.Create(nameof(Clear), typeof(bool), typeof(SketchView), false, defaultBindingMode: BindingMode.TwoWay);
public bool Clear
{
get => (bool)GetValue(ClearProperty);
set
{
SetValue(ClearProperty, value);
if (value) { OnClear(); }
}
}
public void OnClear()
{
MessagingCenter.Send(this, nameof(OnClear));
}
public void SetSignature(string signature)
{
Signature = signature;
}
void IDoubleTappedController.DoubleTapped()
{
throw new NotImplementedException();
}
void ISketchViewController.SendSketchUpdated(string signature)
{
Clear = false;
Signature = signature;
}
}
I have also tried using the SetValueFromRenderer() method from my Custom renderer, again, without success.
May you suggest to me what is the way to set an Element value from a Custom Renderer?
Thanks and kind regards,
Temo
The problem was that the field in my view model was set to null when comparing it with the value. Then throwing a TargetException letting the source buggy unable to be updated by the target.
public bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = default)
{
if (value == null) return false;
if (field != null && field.Equals(value)) return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
Now, I make sure the field is not null before using the Equals operator.

Draw rectangle with text inside

I want to make an application in which the user could add rectangle with customizable text inside it. The rectangle also can have another rectangles inside. Just as you can see on these picture:
I read about DrawingVisual, Shapes etc. So far I did it using DrawingVisual + Host, which derivies from FrameworkElement. DrawingVisual has FormattedText field, and list of Children elements; Host maintain drawing all elements.
The main problem is that, everytime user changes text in any child element I need to calculate new coordinates, width, height of all child elements. Maybe there is any method to do that automatically?
Also, DrawingVisual doesn't have any mouse events. So how to make all elements selectable / hoverable? Or should I derive from some other class?
Later I will post some code...
EDIT:
public class VisualHost: FrameworkElement
{
private VisualCollection _children;
private List<MyElement> _list;
public VisualHost(List<MyElement> list)
{
_children = new VisualCollection(this);
_list = list;
}
protected override int VisualChildrenCount
{
get { return _children.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= _children.Count)
{
throw new ArgumentOutOfRangeException();
}
return _children[index];
}
private void CheckSize(MyElement element)
{
double sw = 0;
double mh = 0;
if (element.GetChildCount() > 0)
{
for (int i = 0; i < element.GetChildCount(); i++)
{
CheckSize(element.GetChild(i));
sw += element.GetChild(i).Width;
mh = Math.Max(mh, element.GetChild(i).Height);
}
}
element.Width = Math.Max(element.Formatted.Width, sw);
element.Height = element.Formatted.Height + mh;
}
private void DrawElement(double top, double left, MyElement element)
{
CheckSize(element);
var context = element.RenderOpen();
context.DrawRectangle( null, new Pen(Brushes.Black, 2d), new Rect(new Point(left, top), new Size(element.Width, element.Height)));
context.DrawText(element.Formatted, new Point(left, top));
top += element.Formatted.Height;
if (element.GetChildCount() > 0)
{
for (int i = 0; i < element.GetChildCount(); i++)
{
context.DrawRectangle(null, new Pen(Brushes.Black, 2d), new Rect(new Point(left, top), new Size(element.GetChild(i).Width, element.GetChild(i).Height)));
context.DrawText(element.GetChild(i).Formatted, new Point(left, top));
left += element.GetChild(i).Width;
}
}
context.Close();
_children.Add(element);
}
public void Redraw()
{
if (_list != null)
{
double top = 0, left = 0;
foreach (MyElement element in _list)
{
DrawElement(top, left, element);
top += element.Height + 10d;
}
}
}
}
public class MyElement: DrawingVisual
{
private string _text;
public string Text
{
get { return _text; }
set {
if (_text != value)
{
Typeface typeface = new Typeface(new FontFamily("Arial"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
Formatted = new FormattedText(value, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, Brushes.Red);
_text = value;
}
}
}
public FormattedText Formatted { get; private set; }
public double Height { get; set; }
public double Width { get; set; }
private List<MyElement> _children;
public MyElement GetChild(int i)
{
if (i < 0 || i >= _children.Count)
{
throw new ArgumentOutOfRangeException();
}
return _children[i];
}
public int GetChildCount()
{
return _children.Count;
}
public void AddChild(MyElement child)
{
_children.Add(child);
}
public MyElement(string Text)
{
this.Text = Text;
this._children = new List<MyElement>();
}
}
MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
_list = new List<MyElement>();
_list.Add(new MyElement("text"));
var e = new MyElement("text 2");
e.AddChild(new MyElement("a"));
e.AddChild(new MyElement("b"));
e.AddChild(new MyElement("c"));
_list.Add(e);
_host = new VisualHost(_list);
MyCanvas.Children.Add(_host);
_host.Redraw();
}
This is my code for now. I wrote it only to check if idea is correct.
well I'm not sure if you would like this approach but you can actually do it very simple... I'm thinking maybe you can use blend to create a user control and design a label and a listbox in a stackpanel and set 'em all on autosizing.
or design 2 stack panels set 1 to do vertical orientation and the other one to do horizontal and add textblocks or something to the horizontal one.

Can not find child control on ItemContainerGenerator?

I'm using ItemsControl for StackPanel as below code:
File playBackControl.xaml - Begin
<ScrollViewer x:Name="scrollViewerChannelBtns">
<StackPanel x:Name="channelBtns" Orientation="Vertical" MouseWheel="ScrollViewer_MouseWheel">
<ItemsControl x:Name="channelBtnItems" ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ToogleButton x:Name="tgbChannelName"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="{Binding Path=ChannelNameBtnWidth}"
Height="{Binding Path=ChannelNameBtnHeight}"
Margin="{Binding Path=ChannelNameBtnMargin}"
IsChecked="{Binding Path=IsChecked, Mode=TwoWay}"
ToolTip="{Binding Path=ToolTip}" Tag="{Binding Path=Index}"
IsEnabled="{Binding Path=IsEnabled}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
File playBackControl.xaml - End
File playBackControl.xaml.cs - Begin
public partial class PlayBackControl : UserControl
{
public static List<ChannelBtnItemData> listChannelBtnItemData = new List<ChannelBtnItemData>();
public PlayBackControl() //This will run first when the app start
{
InitializeComponent();
channelBtnItems.ItemContainerGenerator.StatusChanged += ChannelBtnItemsStatusChangedEventHandler;
System.Threading.Thread threadTimer = new System.Threading.Thread(TimerThreadThreadProc);
threadTimer.Start();
}
private void TimerThreadThreadProc()
{
while (true)
{
Thread.Sleep(60000); //Sleep 60s
this.Dispatcher.BeginInvoke(new Action(delegate()
{
//Re init listChannelBtnItemData, this list has about 64 items
channelBtnItems.ItemsSource = listChannelBtnItemData;
channelBtnItems.Items.Refresh();
}));
}
}
public class ChannelBtnItemData : INotifyPropertyChanged
{
private String _toolTip;
private int _index;
private int _channelID;
private bool _isChecked;
private bool _isEnabled;
private bool _lockToggle;
private double _channelNameBtnWidth;
private double _channelNameBtnHeight;
private Thickness _channelNameBtnMargin;
public String ToolTip
{
get { return _toolTip; }
set
{
_toolTip = value;
OnPropertyChanged("ToolTip");
}
}
public int Index
{
get { return _index; }
set
{
_index = value;
OnPropertyChanged("Index");
}
}
public int ChannelID
{
get { return _channelID; }
set
{
_channelID = value;
OnPropertyChanged("ChannelID");
}
}
public bool IsChecked
{
get { return _isChecked; }
set
{
_isChecked = value;
OnPropertyChanged("IsChecked");
}
}
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
OnPropertyChanged("IsEnabled");
}
}
public bool LockToggle
{
get { return _lockToggle; }
set
{
_lockToggle = value;
OnPropertyChanged("LockToggle");
}
}
public double ChannelNameBtnWidth
{
get { return _channelNameBtnWidth; }
set
{
_channelNameBtnWidth = value;
OnPropertyChanged("ChannelNameBtnWidth");
}
}
public double ChannelNameBtnHeight
{
get { return _channelNameBtnHeight; }
set
{
_channelNameBtnHeight = value;
OnPropertyChanged("ChannelNameBtnHeight");
}
}
public Thickness ChannelNameBtnMargin
{
get { return _channelNameBtnMargin; }
set
{
_channelNameBtnMargin = value;
OnPropertyChanged("ChannelNameBtnMargin");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public ChannelBtnItemData()
{
}
}
private void ChannelBtnItemsStatusChangedEventHandler(Object sender, EventArgs e)
{
if (channelBtnItems.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
if (channelBtnItems.HasItems)
{
var containers = channelBtnItems.Items.Cast<Object>().Select(
item => (FrameworkElement)channelBtnItems.ItemContainerGenerator.ContainerFromItem(item));
foreach (var container in containers)
{
if (container != null)
container.Loaded += ChannelBtnItemContainerLoaded;
}
}
}
}
private void ChannelBtnItemContainerLoaded(object sender, RoutedEventArgs e)
{
var element = (FrameworkElement)sender;
element.Loaded -= ChannelBtnItemContainerLoaded;
ToogleButton tgbChannelName = FindChild<ToogleButton>(element, "tgbChannelName");
if (tgbChannelName != null) //Sometimes It equal null
{
//Do something
}
}
public T FindChild<T>(DependencyObject parent, string childName)
where T : DependencyObject
{
// Confirm parent and childName are valid.
if (parent == null) return null;
T foundChild = null;
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is not of the request child type child
T childType = child as T;
if (childType == null)
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
else if (!string.IsNullOrEmpty(childName))
{
var frameworkElement = child as FrameworkElement;
// If the child's name is set for search
if (frameworkElement != null && frameworkElement.Name == childName)
{
// if the child's name is of the request name
foundChild = (T)child;
break;
}
else
{
// recursively drill down the tree
foundChild = FindChild<T>(child, childName);
// If the child is found, break so we do not overwrite the found child.
if (foundChild != null) break;
}
}
else
{
// child element found.
foundChild = (T)child;
break;
}
}
return foundChild;
}
}
File playBackControl.xaml.cs - End
In the ChannelBtnItemContainerLoaded() function, sometimes the tgbChannelName equal null,
I have searched and read more about this, but I don't know how to fix it.
I create a demo to test this but it worked for me. Loaded event is being fired when all is there available to access and so I never ran into the state where instance was null.
However I have a feeling that you haven't revealed us what you exactly up to there.
Are you changing the ItemsSource at runtime? At what point do you swap the ItemsSource?
Where is the following code of yours being called?
channelBtnItems.ItemContainerGenerator.StatusChanged += ChannelBtnItemsStatusChangedEventHandler;
channelBtnItems.ItemsSource = listChannelBtnItemData;
channelBtnItems.Items.Refresh();
However here is a trick how you can "postpone" an action.
Use the Dispatcher.BeginInvoke with DispatcherPrority.Background.
http://weblogs.asp.net/pawanmishra/archive/2010/06/06/understanding-dispatcher-in-wpf.aspx
private void ChannelBtnItemContainerLoaded(object sender, RoutedEventArgs e)
{
var element = (FrameworkElement)sender;
element.Loaded -= ChannelBtnItemContainerLoaded;
element.Dispatcher.BeginInvoke((Action)(() =>
{
ToggleButton tgbChannelName = FindChild<ToggleButton>(element, "tgbChannelName");
if (tgbChannelName != null) //Sometimes It equal null
{
//Do something
}
else
{
}
}), DispatcherPriority.Background);
}
Try it out. If this doesnt help please provide us with the complete code including data and viewmodel so can test on same code just like yours.

How does a container know when a child has called InvalidateArrange?

I'm trying to learn the basics of creating a custom panel in a WinRT XAML app. I have defined an attached dependency property and it's working as expected except i can't figure out how to get the property's callback for a child element to trigger the arrange or measure of the container.
What's the proper way to for a child to let it's container know that arrange and measure should be called again? In my WPF 4 unleashed book they use the FrameworkPropertyMetadataOptions.AffectsParentArrange but that doesn't seem to be available in WinRT.
public class SimpleCanvas : Panel
{
#region Variables
#region Left Property
public static double GetLeft(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
object value = element.GetValue(LeftProperty);
Type valueType = value.GetType();
return Convert.ToDouble(value);
}
public static void SetLeft(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(LeftProperty, value);
}
public static readonly DependencyProperty LeftProperty =
DependencyProperty.RegisterAttached("Left", typeof(double), typeof(SimpleCanvas),
new PropertyMetadata(0, OnLeftPropertyChanged));
public static void OnLeftPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)source;
// This doesn't cause ArrangeOverride below to be called
element.InvalidateArrange();
}
#endregion
#region Top Property
public static double GetTop(UIElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
object value = element.GetValue(TopProperty);
return (value == null) ? 0 : (double)value;
}
public static void SetTop(UIElement element, double value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(TopProperty, value);
}
public static readonly DependencyProperty TopProperty =
DependencyProperty.RegisterAttached("Top", typeof(double), typeof(SimpleCanvas),
new PropertyMetadata(0, OnTopPropertyChanged));
public static void OnTopPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)source;
// This doesn't cause ArrangeOverride below to be called
element.InvalidateArrange();
}
#endregion
#endregion
public SimpleCanvas()
{
}
#region Methods
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in this.Children)
{
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
return new Size(0, 0);
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in this.Children)
{
double x = 0;
double y = 0;
double left = GetLeft(child);
double top = GetTop(child);
if (!double.IsNaN(left))
{
x = left;
}
if (!double.IsNaN(top))
{
y = top;
}
child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
}
return finalSize;
}
#endregion
}
I'm late to the party, but I went the same direction and faced the same issue. Here is my solution.
In your callback you call InvalidateArrange on the child element you attached you property to:
public static void OnTopPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)source;
// This doesn't cause ArrangeOverride below to be called
element.InvalidateArrange();
}
But you should really invalidate the panel, by changing you code so:
public static void OnTopPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
UIElement panel= VisualTreeHelper.GetParent(source) as UIElement;
if(panel != null)
panel.InvalidateArrange();
}
And it should work (did for me).
If InvalidateArrange alone doesn't work you could also try InvalidateMeasure or UpdateLayout.
I had this problem with a child control which depended on the FontSize of the parent control. I solved the problem by traveling up the stack of parents and invalidating everything:
static MyControl()
{
// replace base implementation of the dependent property
FontSizeProperty.OverrideMetadata(typeof(Scalar),
new FrameworkPropertyMetadata(SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits, OnMeasureInvalidated));
}
private static void OnMeasureInvalidated(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
// recurse over parent stack
while (true)
{
var parent = VisualTreeHelper.GetParent(sender) as UIElement;
if (parent == null) return; // break on root element
parent.InvalidateMeasure();
sender = parent;
}
}

How to redraw Node in TreeView (WinForms)

I need to implement own TreeView with blinked TreeNode. My prototype is:
public class BlinkTreeView : TreeView
{
private int blinkInterval;
private bool blinkState;
[Category("Behavior"), Browsable(true)]
public Icon BlinkIcon { get; set; }
[Category("Behavior"), Browsable(true)]
public Icon SelectedBlinkIcon { get; set; }
[Category("Behavior"), Browsable(true), DefaultValue(1000)]
public int BlinkInterval {
get
{
return blinkInterval;
}
set
{
blinkInterval = value;
if (value > 0)
{
blinkTimer.Interval = value;
blinkTimer.Start();
}
else
{
blinkTimer.Stop();
blinkState = false;
Invalidate();
}
}
}
private Timer blinkTimer;
public BlinkTreeView()
: base()
{
blinkTimer = new Timer();
blinkTimer.Tick += new EventHandler(blinkTimer_Tick);
blinkState = false;
this.DrawMode = TreeViewDrawMode.OwnerDrawAll;
}
void blinkTimer_Tick(object sender, EventArgs e)
{
if (BlinkInterval > 0)
{
blinkState = !blinkState;
}
else
{
blinkState = false;
}
Invalidate();
}
protected override void OnDrawNode(DrawTreeNodeEventArgs e)
{
e.DrawDefault = true;
base.OnDrawNode(e);
if (blinkState)
{
//here i want to draw blinked item, but i can't redraw item icons and text.
}
}
}
In OnDrawNode i can't redraw icon and text of node.
Any idea how to solve this?
Just a thought, but you could invert (xor) over the item without making the tree into an owner-draw control. I think it works something like the following:
using (Graphics g = Graphics.FromHwnd(Tree.Handle))
{
TreeNode node = myBlinkyNode;
if (node != null)
{
using(Region myRegion = new Region(node.Bounds))
myRegion.Xor(xorRect);
}
}
You'll need to keep track if the blink is visible or not and handle the Paint event so that you can re-draw the inverted rectangle.
Have a timer toggle the state of the blinking nodes, i.e.:
Node.ForeColor = Node.ForeColor == Color.White ? Color.Black : Color.White;

Categories