I created dummy custom panel ShelfPanel with attached property Exact influence panel arrange:
class ShelfPanel : Panel
{
#region Start attached property
public static DependencyProperty ExactProperty = DependencyProperty.RegisterAttached("Exact", typeof(int), typeof(ShelfPanel),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsArrange | // Does this options have auto action?
FrameworkPropertyMetadataOptions.AffectsRender)); // Does this options have auto action?
public static void SetExact(UIElement element, int value)
{
element.SetValue(ExactProperty, value);
}
public static int GetExact(UIElement element)
{
return (int)element.GetValue(ExactProperty);
}
#endregion
protected override Size MeasureOverride(Size availableSize)
{
Size final = new Size();
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
final.Height += child.DesiredSize.Height;
}
return final;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in InternalChildren)
{
Point position = new Point();
int exact = ShelfPanel.GetExact(child);
// Calculate position based on attached Exact
position.X = exact * 100;
position.Y = exact * 100;
child.Arrange(new Rect(position, child.DesiredSize));
}
return finalSize;
}
}
<local:ShelfPanel>
<local:Box local:ShelfPanel.Exact="0" MouseDown="Box_MouseDown"/>
<local:Box local:ShelfPanel.Exact="1" />
<local:Box local:ShelfPanel.Exact="2" />
</local:ShelfPanel>
public partial class MainWindow : Window // Codebehind for previous xaml
{
public MainWindow()
{
InitializeComponent();
}
private void Box_MouseDown(object sender, MouseButtonEventArgs e)
{
// I certainly sure this code get triggered after click.
ShelfPanel.SetExact(sender as UIElement, 3);
}
}
This works perfect <local:Box> are arranged as planned.
As you can deduce from code, after click on first <local:Box> it should change it position to 3, just after others 2. But surprisingly nothing happen.
Doesn't FrameworkPropertyMetadataOptions.Affects Arrange or FrameworkPropertyMetadataOptions.AffectsRender automatically repaint panel?
To make this works I need to add PropertyChangedCallbackand call there InvalidateVisual()?
You are setting the attached property on a Box, but want the parent element of the Box, i.e. the ShelfPanel to be arranged.
You should therefore set FrameworkPropertyMetadataOptions.AffectsParentArrange:
public static readonly DependencyProperty ExactProperty =
DependencyProperty.RegisterAttached("Exact", typeof(int), typeof(ShelfPanel),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsParentArrange));
Related
I´ve created my own control inherited from the panel control with OnDrop method for handling Drop event. Unfortunately, the event is fired twice.
I don´t know why.
here my code
public partial class Panel_my : Panel
{
public static readonly DependencyProperty StartDateProperty =
DependencyProperty.RegisterAttached("StartDate", typeof(DateTime), typeof(Panel_my), new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.AffectsParentArrange));
public static readonly DependencyProperty EndDateProperty =
DependencyProperty.RegisterAttached("EndDate", typeof(DateTime), typeof(Panel_my), new FrameworkPropertyMetadata(DateTime.MaxValue, FrameworkPropertyMetadataOptions.AffectsParentArrange));
public static readonly DependencyProperty MaxDateProperty =
DependencyProperty.Register("MaxDate", typeof(DateTime), typeof(Panel_my), new FrameworkPropertyMetadata(DateTime.MaxValue, FrameworkPropertyMetadataOptions.AffectsMeasure));
public static readonly DependencyProperty MinDateProperty =
DependencyProperty.Register("MinDate", typeof(DateTime), typeof(Panel_my), new FrameworkPropertyMetadata(DateTime.MaxValue, FrameworkPropertyMetadataOptions.AffectsMeasure));
public static DateTime GetStartDate(DependencyObject obj)
{
return (DateTime)obj.GetValue(StartDateProperty);
}
public static void SetStartDate(DependencyObject obj, DateTime value)
{
obj.SetValue(StartDateProperty, value);
}
public static DateTime GetEndDate(DependencyObject obj)
{
return (DateTime)obj.GetValue(EndDateProperty);
}
public static void SetEndDate(DependencyObject obj, DateTime value)
{
obj.SetValue(EndDateProperty, value);
}
public DateTime MaxDate
{
get { return (DateTime)GetValue(MaxDateProperty); }
set { SetValue(MaxDateProperty, value); }
}
public DateTime MinDate
{
get { return (DateTime)GetValue(MinDateProperty); }
set { SetValue(MinDateProperty, value); }
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in Children)
{
child.Measure(availableSize);
}
return new Size(0, 0);
}
protected override Size ArrangeOverride(Size finalSize)
{
double range = (MaxDate - MinDate).Ticks;
double pixelsPerTick = finalSize.Width / range;
foreach (UIElement child in Children)
{
ArrangeChild(child, MinDate, pixelsPerTick, finalSize.Height);
}
return finalSize;
}
private void ArrangeChild(UIElement child, DateTime minDate, double pixelsPerTick, double elementHeight)
{
DateTime childStartDate = GetStartDate(child);
DateTime childEndDate = GetEndDate(child);
TimeSpan childDuration = childEndDate - childStartDate;
double offset = (childStartDate - minDate).Ticks * pixelsPerTick;
double width = childDuration.Ticks * pixelsPerTick;
child.Arrange(new Rect(offset, 0, width, elementHeight));
}
protected override void OnDrop(DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.StringFormat))
{
}
e.Handled = true;
}
}
When I place a button on the panel and move it in the drag & drop process and drop it on that panel, the drop event is fired twice.
What can I do to have only one drop event?
I got it. The problem is that the button fires event MouseMove not only when the mouse is moving but even when the left button is pressed without moving the mouse.
I changed the method OnMouseMove and than it works!
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed && this.IsPressed == false)
{
DragDrop.DoDragDrop(this, this.Content, DragDropEffects.Move);
}
}
Thank you, #jdweng very much for help and direction!!!
I have written a custom panel. Here is the code:
public class GameBoardPanel:Panel
{
public GameBoardPanel() { }
public static readonly DependencyProperty SquareSideLengthProperty =
DependencyProperty.Register("SquareSideLength", typeof(int), typeof(GameBoardPanel), new PropertyMetadata(0));
public int SquareSideLength
{
get { return (int)GetValue(SquareSideLengthProperty); }
private set { SetValue(SquareSideLengthProperty, value); }
}
public static readonly DependencyProperty RowsProperty =
DependencyProperty.Register("Rows", typeof(int), typeof(GameBoardPanel), new PropertyMetadata(0));
public int Rows
{
get { return (int)GetValue(RowsProperty); }
set { SetValue(RowsProperty, value); }
}
public int Columns
{
get { return (int)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register("Columns", typeof(int), typeof(GameBoardPanel), new PropertyMetadata(0));
public static readonly DependencyProperty RowProperty =
DependencyProperty.RegisterAttached("Row", typeof(int), typeof(GameBoardPanel));
public static readonly DependencyProperty ColumnProperty =
DependencyProperty.RegisterAttached("Column", typeof(int), typeof(GameBoardPanel));
public static void SetRow(DependencyObject control, int value)
{
control.SetValue(RowProperty, value);
}
public static int GetRow(DependencyObject control)
{
return (int)control.GetValue(RowProperty);
}
public static void SetColumn(DependencyObject control, int value)
{
control.SetValue(ColumnProperty, value);
}
public static int GetColumn(DependencyObject control)
{
return (int)control.GetValue(ColumnProperty);
}
protected override Size MeasureOverride(Size availableSize)
{
double min=Math.Min(availableSize.Width/Columns,availableSize.Height/Rows );
SquareSideLength=(int)min;
return new Size(SquareSideLength*Columns, SquareSideLength*Rows);
}
protected override Size ArrangeOverride(Size finalSize)
{
double min=Math.Min(finalSize.Width/Columns,finalSize.Height/Rows );
SquareSideLength=(int)min;
Size destinationSize = new Size(SquareSideLength * Columns, SquareSideLength * Rows);
foreach (UIElement element in base.InternalChildren)
{
int row = GameBoardPanel.GetRow(element);
int column = GameBoardPanel.GetColumn(element);
element.Arrange(new Rect(column * SquareSideLength, row * SquareSideLength,SquareSideLength, SquareSideLength));
}
return destinationSize;
}
}
It is very simple idea, similar to wpf Grid panel. I have a control which arrange its children on such a panel by setting children GameBoardPanel.Row and GameBoardPanel.Column attached properties. Let's assume that I have also a GameObject class which is a CustomControl and I added it to the panel. I want to move it for example to the left. So I do it in the following way:
GameObject movable=new GameObject();
//setting properties such as GameBoardPanel.Row and GameBoardPanel.Column
panel.Children.Add(movable);
GameBoardPanel.SetColumn(GameBoardPanel.GetColumn(movable)-1);
Let's skip the problem that the movable object can move outside the panel. It's not a big problem. However if I did the movement in that way I didn't see any effect. Only after adding an additional line:
panel.InvalidateArrange();
I observed that the object moved left. I'm quite sure, that if I would do the same with the wpf Grid, it's not necessary to call InvalidateArrange method. So, I am curious why does this happen? :D
You may set appropriate property metadata options to tell the framework that the Row and Column attached properties affect the layout of your custom panel:
public static readonly DependencyProperty RowProperty =
DependencyProperty.RegisterAttached("Row", typeof(int), typeof(GameBoardPanel),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsParentArrange));
public static readonly DependencyProperty ColumnProperty =
DependencyProperty.RegisterAttached("Column", typeof(int), typeof(GameBoardPanel),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsParentArrange));
That said, you should usually also measure the child elements in the MeasureOverride method:
protected override Size MeasureOverride(Size availableSize)
{
double min = Math.Min(availableSize.Width / Columns, availableSize.Height / Rows);
SquareSideLength = (int)min;
foreach (UIElement element in InternalChildren)
{
element.Measure(new Size(SquareSideLength, SquareSideLength));
}
return new Size(SquareSideLength * Columns, SquareSideLength * Rows);
}
Moreover it also doesn't seem to be necessary that SquareSideLength is a dependency property. It could as well be an ordinary CLR property (or probably just a private field). Its type may also better be double instead of int.
Finally, your MeasureOverride implementation should also be prepared for an infinite availableSize parameter value. From MSDN:
The available size that this
element can give to child elements. Infinity can be specified as a
value to indicate that the element will size to whatever content is
available.
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'm using two classes as Toolbox derived from ItemsControl and ToolboxItem derived from ContnentControl
// Implements ItemsControl for ToolboxItems
public class Toolbox : ItemsControl
{
// Defines the ItemHeight and ItemWidth properties of
// the WrapPanel used for this Toolbox
public Size ItemSize
{
get { return itemSize; }
set { itemSize = value; }
}
private Size itemSize = new Size(50, 50);
// Creates or identifies the element that is used to display the given item.
protected override DependencyObject GetContainerForItemOverride()
{
return new ToolboxItem();
}
// Determines if the specified item is (or is eligible to be) its own container.
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is ToolboxItem);
}
}
// Represents a selectable item in the Toolbox/>.
public class ToolboxItem : ContentControl
{
// caches the start point of the drag operation
private Point? dragStartPoint = null;
static ToolboxItem()
{
// set the key to reference the style for this control
FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(
typeof(ToolboxItem), new FrameworkPropertyMetadata(typeof(ToolboxItem)));
}
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseDown(e);
this.dragStartPoint = new Point?(e.GetPosition(this));
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton != MouseButtonState.Pressed)
this.dragStartPoint = null;
if (this.dragStartPoint.HasValue)
{
// XamlWriter.Save() has limitations in exactly what is serialized,
// see SDK documentation; short term solution only;
string xamlString = XamlWriter.Save(this.Content);
DragObject dataObject = new DragObject();
dataObject.Xaml = xamlString;
WrapPanel panel = VisualTreeHelper.GetParent(this) as WrapPanel;
if (panel != null)
{
// desired size for DesignerCanvas is the stretched Toolbox item size
double scale = 1.3;
dataObject.DesiredSize = new Size(panel.ItemWidth * scale, panel.ItemHeight * scale);
}
DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Copy);
e.Handled = true;
}
}
}
// Wraps info of the dragged object into a class
public class DragObject
{
// Xaml string that represents the serialized content
public String Xaml { get; set; }
// Defines width and height of the DesignerItem
// when this DragObject is dropped on the DesignerCanvas
public Size? DesiredSize { get; set; }
}
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
DesignerItem ni = new DesignerItem();
ni.AllowDrop = true;
ni.ToolTip = "tooltip goes here";
ni.Width = 100;
ni.Height = 200;
ni.Background = Brushes.Red;
//somehow code should introduce the object shape as object. in the sample on codeproject it is reading shape information from xaml but I need to add it from code behind.
Toolbox tb = new Toolbox();
tb.Items.Add(ni);
AddChild() method from ItemsControl must be the method to add a new object on a toolbox. However, when I instantiate an object from this class it does not give me this method. For simplicity I only want to add a rectangle shape on it this would allow me to to drag/drop it on canvas. So question is how I can add a Rectangle on toobox.
Any help is appreciated.
Thanks.
Amit
Solution:
var TheToolbar = ToolboxContainer.Content as Toolbox;
// Instantiate a ToolboxItem
ToolboxItem TheToolboxItem = new ToolboxItem();
Rectangle myRect = new Rectangle();
myRect.StrokeThickness = 1;
myRect.Stroke = some value for stroke;
myRect.Fill = some value for filling the object;
myRect.IsHitTestVisible = false;
//add to Toolbar
TheToolboxItem.Content= myRect;
TheToolbar.Items.Add(TheToolboxItem);
AddChild() is a protected method ( = accessible only from itself or from an inherited class). Try to use a code like this instead:
Toolbox tb = new Toolbox();
tb.Items.Add(myNewItem);
After you instantiate your objects, you need to add them as children to your canvas/parent element.
myParentElement.Children.Add(tb);
If you're doing it, include it in your example code.
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