I want to implement my own control derived from FrameworkElement, but the added child elements are not rendered.
I have no idea why.
public class RangeSelection : FrameworkElement
{
private Thumb thumb = null;
#region Construction / Destruction
public RangeSelection()
{
this.thumb = new Thumb();
this.thumb.Width = 32.0;
this.thumb.Height = 32.0;
this.AddVisualChild(this.thumb);
}
#endregion
protected override Size MeasureOverride(Size availableSize)
{
this.thumb.Measure(availableSize);
return new Size(64.0, 64.0);
}
protected override Size ArrangeOverride(Size finalSize)
{
this.thumb.Arrange(new Rect(0, 0, 64.0, 64.0));
return base.ArrangeOverride(finalSize);
}
}
You need to override VisualChildrenCount property and GetVisualChild method. Something like this:
protected override int VisualChildrenCount
{
get { return thumb == null ? 0 : 1; }
}
protected override Visual GetVisualChild(int index)
{
if (_child == null)
{
throw new ArgumentOutOfRangeException();
}
return _child;
}
In case you want more child elements, you should use some kind of collection to store child elements and then you will return collection's count or appropriate element of collection.
Related
I created program to check ability of FrameworkElement to contain TextBox.
namespace MyControls
{
public class FooButton : FrameworkElement
{
public FooButton()
{
Width = 100;
Height = 100;
VisualCollection = new VisualCollection(this);
VisualCollection.Add(new TextBox() { Text = "Meows" });
}
protected override int VisualChildrenCount => VisualCollection.Count;
protected override Visual GetVisualChild(int index) => VisualCollection[index];
public VisualCollection VisualCollection { get; }
}
}
namespace bitmap_test_example
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
Here is XAML file for program:
<Window x:Class="bitmap_test_example.MainWindow"
xmlns:MyControls = "clr-namespace:MyControls"
......
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<MyControls:FooButton></MyControls:FooButton>
<Button>yeah</Button>
</DockPanel>
</Window>
But unfortunately program does not show TextBox. (By the way I checked visual tree, TextBox is indeed contained within custom FooButton)What's the problem with code? Maybe additional methods should be redefined for FrameworkElement?
You should implement the MeasureOverride and ArrangeOverride methods of your custom element. The implementations should measure and arrange the visual children, e.g.:
public class FooButton : FrameworkElement
{
private readonly UIElement _child = new TextBox() { Text = "Meows" };
public FooButton()
{
AddVisualChild(_child);
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index) => index == 0 ?
_child : throw new ArgumentOutOfRangeException();
protected override Size MeasureOverride(Size availableSize)
{
_child.Measure(availableSize);
return _child.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
_child.Arrange(new Rect(finalSize));
return finalSize;
}
}
Why the MessageBox and CheckBox apprears in Design-Time when changing value of the First property and do not when adding item to Second?
private string _first;
[Description(""), Category("GostcompSettings"), DefaultValue("27017")]
public string First
{
get { return __first; }
set
{
_searchAreasChceckBoxList.Clear();
pPanelWithCheckboxies.Controls.Clear();
int x = 10;
int y = 10;
CheckBox _tempCheck = new CheckBox();
_tempCheck.Checked = true;
_tempCheck.Location = new Point(x, y);
_searchAreasChceckBoxList.Add(_tempCheck);
pPanelWithCheckboxies.Controls.Add(_tempCheck);
MessageBox.Show("zmiana");
_first = value;
}
}
private Collection<bool> _second= new Collection<bool>();
[Description(""), Category("*")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Collection<bool> Second
{
get
{
return _second;
}
set
{
_searchAreasChceckBoxList.Clear();
pPanelWithCheckboxies.Controls.Clear();
int x = 10;
int y = 10;
CheckBox _tempCheck = new CheckBox();
_tempCheck.Checked = true;
_tempCheck.Location = new Point(x, y);
_searchAreasChceckBoxList.Add(_tempCheck);
pPanelWithCheckboxies.Controls.Add(_tempCheck);
MessageBox.Show("*");
_second= value;
}
}
It's the same scenario when I change Collection to List...
Values are kept (or added to Collection in Second case) and designer generate code for InitializeComponent().
EDIT after #taffer answear
public class SearchAreaInfo
{
public SearchAreasEnum searchArea
{
get; set;
}
}
public class SearchAreaInfoCollection : Collection<SearchAreaInfo>
{
private Panel _checkboxParent;
public SearchAreaInfoCollection(Panel checkboxParent) : base()
{
_checkboxParent = checkboxParent;
}
// called on Add/Insert
protected override void InsertItem(int index, SearchAreaInfo item)
{
base.InsertItem(index, item);
RepaintChackboxPanel();
}
// called on Remove/RemoveAt
protected override void RemoveItem(int index)
{
base.RemoveItem(index);
RepaintChackboxPanel();
}
// called when an element is set by the indexer
protected override void SetItem(int index, SearchAreaInfo item)
{
base.SetItem(index, item);
RepaintChackboxPanel();
}
private void RepaintChackboxPanel()
{
//_searchAreasChceckBoxList.Clear();
_checkboxParent.Controls.Clear();
int x = 0;
int y = 0;
foreach (var item in this)
{
CheckBox _tempCheck = new CheckBox();
_tempCheck.Checked = true;
_tempCheck.Location = new Point(x, y);
_tempCheck.BringToFront();
//_searchAreasChceckBoxList.Add(_tempCheck);
_checkboxParent.Controls.Add(_tempCheck);
x += 5;
y += 5;
}
_checkboxParent.Invalidate();
}
}
private SearchAreaInfoCollection _searchAreas;
[Description(""), Category("GostcompSettings")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public SearchAreaInfoCollection SearchAreas
{
get
{
return _searchAreas;
}
}
Now the prooblem is that: when I add let's say third item to the collection in Editor it draws only one checkbox but should draw 3 checkboxes...
Moreover in debugging I see that foreach loop goes then 3 times: 1st collection has 1 item , 2nd time collection has 2 items and third time collection has 3 items, but finally I see just one checkbox in _checkboxPanel.
Because the setter of Second property is executed only when you replace the whole collection. When you add/remove an item, the getter returns your collection instance (_second), and the Add/Remove method will be called on that object instance.
If you want to perform checks on element addition/removal, create a custom collection type instead:
public class MyBoolCollection: Collection<bool>
{
// called on Add/Insert
protected override void InsertItem(int index, bool item)
{
// do some checks here
base.InsertItem(index, item);
}
// called on Remove/RemoveAt
protected override void RemoveItem(int index)
{
// do some checks here
base.RemoveItem(index, item);
}
// called when an element is set by the indexer
protected override void SetItem(int index, bool item)
{
// do some checks here
base.SetItem(index, item);
}
}
The built-in collection editor that you are using now only changes the content of your collection object. Which works fine, but does not get your setter called at all. In other words, it never creates a new collection object, nor does it know how to do that.
To get your property setter called, you have to create your own UITypeEditor and have it return a new collection from its EditValue() method override. Pretty easy to do, first add a reference to System.Design, then make your code look similar to this:
using System.Drawing.Design;
...
[Editor(typeof(MyEditor), typeof(System.Drawing.Design.UITypeEditor))]
public Collection<bool> Second {
// etc...
}
private class MyEditor : UITypeEditor {
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) {
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) {
var editor = new System.ComponentModel.Design.CollectionEditor(typeof(Collection<bool>));
var retval = (Collection<bool>)editor.EditValue(context, provider, value);
return new Collection<bool>(retval);
}
}
You probably want to improve this a bit, like implementing your own editor UI so all of those bools are easier to interpret.
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;
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;
}
}
I want to write a custom Panel that would make use of Decorator patern. That means it will have some other Panel as a property. When some element is added to my custom panel I want to add it also to decorated panel (stored in property) and when some element is removed, I want to remove it from decorated panel as well. How do I do that ?
Is there some method that is to be overriden or some event is fired when change to InternalCholdrens happen ?
Thank you
EDIT: Basicly I want to do something like this I want to turn any panel to the animated one. So I want to decorate any panel with my decorator so it becomes animated.
You can't do that, unfortunately.
What you will get immediately is an exception saying a control can have only one logical parent.
Although what you can do is to do double-delegation. Your panel delegates measure/arrange to another panel, and in return it provides it with 'ghosts' which will act as a children to it and delegate their own measure/arrange to your panel's children.
Here's the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
public class DelegatePanel : Panel
{
private sealed class DelegateChild : FrameworkElement
{
readonly Func<Size, Size> measure;
readonly Func<Size, Size> arrange;
public DelegateChild(Func<Size,Size> measure, Func<Size,Size> arrange)
{
this.measure = measure;
this.arrange = arrange;
}
protected override Size MeasureOverride(Size availableSize)
{
return measure(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
return arrange(finalSize);
}
}
readonly Dictionary<UIElement, UIElement> delegateByChild = new Dictionary<UIElement,UIElement>();
public Panel LayoutPanel
{
get { return (Panel)GetValue(LayoutPanelProperty); }
set { SetValue(LayoutPanelProperty, value); }
}
public static readonly DependencyProperty LayoutPanelProperty =
DependencyProperty.Register("LayoutPanel", typeof(Panel), typeof(DelegatePanel), new PropertyMetadata(null));
protected override Size MeasureOverride(Size availableSize)
{
if(this.LayoutPanel==null)
return base.MeasureOverride(availableSize);
this.delegateByChild.Clear();
this.LayoutPanel.Children.Clear();
foreach (UIElement _child in this.Children)
{
var child = _child;
var delegateChild = new DelegateChild(
availableChildSize =>
{
child.Measure(availableChildSize);
return child.DesiredSize;
},
finalChildSize =>
{
return finalChildSize;
});
delegateByChild[child] = delegateChild;
this.LayoutPanel.Children.Add(delegateChild);
}
this.LayoutPanel.Measure(availableSize);
return this.LayoutPanel.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
if(this.LayoutPanel==null)
return base.ArrangeOverride(finalSize);
this.LayoutPanel.Arrange(new Rect(finalSize));
foreach (var kv in delegateByChild)
{
var child = kv.Key;
var delegateChild = kv.Value;
var position = delegateChild.TranslatePoint(default(Point), this.LayoutPanel);
Rect finalChildBounds = new Rect(
position,
delegateChild.RenderSize);
child.Arrange(finalChildBounds);
}
return this.LayoutPanel.RenderSize;
}
}
Disclaimer: this doesn't implement VirtualizingPanel. So whilst it does work inside ItemsControl and the gang -- it won't perform quick enough for large collections.
i don't totally understand the context for this question but you can override OnVisualChildrenChanged
http://msdn.microsoft.com/en-us/library/system.windows.controls.panel.onvisualchildrenchanged.aspx