Geometry path dependency processing - c#
I have Custom Control Library and do icon "control". Also i have PathGeometry figures accessible by x:Key. So as example input is IconName="Icon_Close" in MainWindow.xaml, i want to equate IconName="Icon_Close" with PathGeometry figure which x:Key equals to "Icon_Close" and show icon
But i have no ideas how to process/convert it...
Icon.xaml (Generic)
<PathGeometry x:Key="Icon_Close" Figures="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/>
<PathGeometry x:Key="Icon_WndMinimize" Figures="M19,13H5V11H19V13Z"/>
<PathGeometry x:Key="Icon_WndMaximize" Figures="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z"/>
<Style TargetType="{x:Type local:Icon}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Icon}">
<Viewbox Height="{TemplateBinding IconHeight}"
Width="Auto">
<Canvas Height="24"
Width="24">
<Path Fill="{TemplateBinding IconColor}"
Data="{TemplateBinding IconName}"/>
</Canvas>
</Viewbox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Icon.cs
public class Icon : Control
{
public static readonly DependencyProperty IconNameProperty =
DependencyProperty.Register("IconName", typeof(Geometry), typeof(Icon),
new PropertyMetadata(Geometry.Empty));
public Geometry IconName
{
get { return (Geometry)GetValue(IconNameProperty); }
set { SetValue(IconNameProperty, value); }
}
static Icon()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Icon),
new FrameworkPropertyMetadata(typeof(Icon)));
}
}
MainWindow.xaml
<uiLib:Icon IconHeight="100"
IconColor="White"
IconName="Icon_Close"/>
If you instead use a Path you can do everything your control appears to be doing with the exception of setting the data with your path.
A Path can stretch so there's no point in the viewbox.
You don't need to put a path in a canvas either, it inherits from frameworkelement.
Here's an example path.
<Path Data="{StaticResource Musketeer}"
Height="18"
Stretch="Uniform"
Fill="Black"
Margin="4,0,0,0" />
Make the height 30 and the musketeer shaped geometry would be 30 px high and retain the proportions of the geometry.
All you need is a dependency property with an onChange handler that finds that geometry out resources.
Path is sealed so you can't extend it, but you could add an attached dependency property like this:
public class Attacher : DependencyObject
{
public static readonly DependencyProperty
GeometryProperty = DependencyProperty.RegisterAttached(
"Geometry", typeof(string), typeof(Attacher), new PropertyMetadata("", GeometryChanged));
private static void GeometryChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var path = d as Path;
if (path == null)
{
return;
}
var geom = path.TryFindResource(e.NewValue.ToString()) as Geometry;
if (geom is Geometry)
{
path.Data = geom;
}
}
public static string GetGeometry(DependencyObject d)
{
return (string)d.GetValue(GeometryProperty);
}
public static void SetGeometry(DependencyObject d, string value)
{
d.SetValue(GeometryProperty, value);
}
}
You can probably think of a better name than attacher.
The way that works is it attaches the dependency property to the path.
Usage:
<Path Fill="Red"
Stretch="Fill"
local:Attacher.Geometry="BeeIcon"
/>
local is an xmlns reference to the clr namespace the attacher is in.
My beeicon is:
<Geometry x:Key="BeeIcon">
M15.551045,25.144995L16.748029,25.144995 16.249038,28.637C16.249038,28.637,15.651051,25.543997,15.551045,25.144995z M21.237988,21.152998L21.437999,21.152998C20.340014,23.049002 18.345033,24.247 16.150041,24.247 13.95404,24.247 11.959059,23.049002 10.861073,21.252997L10.96108,21.252997C15.351034,22.550002,18.943021,21.952002,21.237988,21.152998z M21.337995,14.967002L21.437999,15.067001C21.936988,15.965002,22.235997,16.962996,22.235997,18.160002L22.235997,18.658995 22.036994,18.759001C17.746037,20.653998,12.458048,19.556998,10.16308,18.858L10.063075,18.858 10.063075,18.759001 10.063075,18.160002C10.063075,17.162001,10.263086,16.164,10.761068,15.365997L10.861073,15.365997C16.150041,17.560996,20.141011,15.665,21.337995,14.967002z M16.150041,12.072998C17.746037,12.072998,19.243007,12.671997,20.340014,13.769996L20.44002,13.869003 20.340014,13.869003C17.247018,15.365997 13.356052,14.568 11.560075,14.069 12.657052,12.772003 14.353055,12.072998 16.150041,12.072998z M27.523957,8.3820033C28.821952,8.3820033 29.918929,8.6809992 30.517924,9.3789973 32.91293,12.072998 32.712918,16.663001 27.623963,15.566001 22.534976,14.468002 18.345033,11.674003 17.646031,10.875999L19.043027,10.376998C19.143001,10.376998,23.93197,8.3820033,27.523957,8.3820033z M4.5751051,8.3820033C8.0670862,8.3820033,12.957068,10.376998,12.957068,10.376998L14.353055,10.875999C13.655062,11.674003 9.4640798,14.468002 4.3751249,15.566001 -0.71285403,16.663001 -0.91286493,12.072998 1.4821384,9.3789973 2.1801321,8.5810008 3.2781166,8.3820033 4.5751051,8.3820033z M16.150041,3.2929987C18.145022,3.292999 19.64202,4.8889994 19.64202,6.7850031 19.64202,8.7809977 18.045016,10.277 16.150041,10.277 14.154051,10.277 12.657052,8.6809992 12.657052,6.7850031 12.557047,4.8889994 14.154051,3.292999 16.150041,3.2929987z M12.757057,1.0979993L13.056066,1.5960004 12.957068,1.5960004C12.857062,1.5960005 12.657052,1.4970015 12.557047,1.3970029 12.557047,1.2969969 12.657052,1.0979994 12.757057,1.0979993z M19.542016,9.6861186E-11C19.741996,-6.1625087E-08 19.840995,-6.1625087E-08 20.041005,0.099998488 20.839003,0.399002 21.237988,1.2969969 20.839003,2.0950009 20.44002,3.093002 19.342006,3.5920026 18.345033,3.1930003L18.943021,1.996002 19.043027,1.996002C19.44201,2.0950009 19.840995,1.8960036 19.940999,1.4970014 20.041005,1.1969984 19.940999,0.79800405 19.64202,0.69799812 19.342006,0.59899892 19.143001,0.69799794 18.943021,0.99800076 18.856397,1.1721237 18.921365,1.3470176 19.072932,1.4550116L19.129043,1.4886372 19.44201,1.0979993C19.542016,1.1969984 19.64202,1.2969969 19.542016,1.3970029 19.44201,1.4970015 19.342006,1.5960005 19.143001,1.5960004L19.103326,1.5960004 18.943021,1.996002C18.444032,1.7959975 18.245027,1.1969984 18.444032,0.69799812 18.644011,0.29900353 19.043027,-6.1625087E-08 19.542016,9.6861186E-11z M12.757057,9.6861186E-11C13.256047,-6.1625087E-08 13.655062,0.29900353 13.855041,0.79800428 14.054046,1.2969969 13.855041,1.8960036 13.356052,2.0950009L13.056066,1.5960004C13.256047,1.4970015 13.356052,1.2969969 13.256047,1.0979993 13.156041,0.79800405 12.857062,0.69799794 12.557047,0.79800428 12.258068,0.89800252 12.059064,1.2969969 12.258068,1.5960004 12.458048,1.996002 12.857062,2.1949994 13.156041,2.0950009L13.256047,2.0950009 13.855041,3.1930003C12.857062,3.5920026 11.759047,3.093002 11.360063,2.0950009 11.061054,1.2969969 11.460069,0.399002 12.158062,0.099998488 12.358073,-6.1625087E-08 12.557047,-6.1625087E-08 12.757057,9.6861186E-11z
</Geometry>
But it also works fine with your style of defining streamgeometries.
<Path Fill="Red"
Stretch="Uniform"
Height="32"
local:Attacher.Geometry="Icon_Close"
/>
You can then bind fill, stroke, height, stretch etc and a string for the geometry.
Related
Easy Drag Drop implementation MVVM
I am new to MVVM and I am currently trying to add the drag/drop feature to my application. The thing is I already developed the interface in the code-behind but I am trying now to re-write the code into MVVM as I am only at the beginning of the project. Here is the context: the user will be able to add boxes (ToggleButton but it may change) to a grid, a bit like a chessboard. Below is the View Model I am working on: <Page.Resources> <Style TargetType="{x:Type local:AirportEditionPage}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Page}"> <!-- The page content--> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="{Binding ToolKitWidth, FallbackValue=50}" /> <ColumnDefinition Width="*"/> <ColumnDefinition Width="{Binding RightPanelWidth, FallbackValue=400}"/> </Grid.ColumnDefinitions> <!-- The airport grid where Steps and Links are displayed --> <ScrollViewer Grid.ColumnSpan="4" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <Viewbox Height="{Binding AirportGridHeight}" Width="{Binding AirportGridWidth}" RenderOptions.BitmapScalingMode="HighQuality"> <ItemsControl x:Name="ChessBoard" ItemsSource="{Binding Items}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas Width="{Binding CardQuantityRow}" Height="{Binding CardQuantityColumn}" Background="{StaticResource AirportGridBackground}"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Grid Width="1" Height="1"> <ToggleButton Style="{StaticResource StepCardContentStyle}"/> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemContainerStyle> <Style> <Setter Property="Canvas.Left" Value="{Binding Pos.X}"/> <Setter Property="Canvas.Top" Value="{Binding Pos.Y}" /> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl> </Viewbox> </ScrollViewer> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </Page.Resources> Items are basically from a class (child of INotifiedPropertyChanged) with a name, an icon and a position (Point). Now, I am trying to make the user able to drag and drop the box (ToggleButton) within the grid wherever he/she wants. However, I am totally lost with Commands, AttachedProperties etc. I spent all the whole day on tutorials and tried drag/drop solutions but with my poor knowledge, I don't know how to apply all of this into my code. On my code-behinded version of the code, it was easy. When the button is left-clicked, I say to a variable of the grid "hey, I am being dragged and dropped". While the user is moving, I changed the Item coordinates and when the user released the left button (left button up), the dragdrop_object variable comes null again. In the frame of the MVVM, I am totally lost. Could you give me some tracks to help me trough ? I intended to give up with MVVM a lot of time, but I know that it is better to keep up even if every little feature takes litteraly hours for me to implement (it should decrease with time...). Do not hesitate if you need further details to answer to my question.
I found the solution here : Move items in a canvas using MVVM and here : Combining ItemsControl with draggable items - Element.parent always null To be precise, here is the code I added : public class DragBehavior { public readonly TranslateTransform Transform = new TranslateTransform(); private static DragBehavior _instance = new DragBehavior(); public static DragBehavior Instance { get { return _instance; } set { _instance = value; } } public static bool GetDrag(DependencyObject obj) { return (bool)obj.GetValue(IsDragProperty); } public static void SetDrag(DependencyObject obj, bool value) { obj.SetValue(IsDragProperty, value); } public static readonly DependencyProperty IsDragProperty = DependencyProperty.RegisterAttached("Drag", typeof(bool), typeof(DragBehavior), new PropertyMetadata(false, OnDragChanged)); private static void OnDragChanged(object sender, DependencyPropertyChangedEventArgs e) { // ignoring error checking var element = (UIElement)sender; var isDrag = (bool)(e.NewValue); Instance = new DragBehavior(); ((UIElement)sender).RenderTransform = Instance.Transform; if (isDrag) { element.MouseLeftButtonDown += Instance.ElementOnMouseLeftButtonDown; element.MouseLeftButtonUp += Instance.ElementOnMouseLeftButtonUp; element.MouseMove += Instance.ElementOnMouseMove; } else { element.MouseLeftButtonDown -= Instance.ElementOnMouseLeftButtonDown; element.MouseLeftButtonUp -= Instance.ElementOnMouseLeftButtonUp; element.MouseMove -= Instance.ElementOnMouseMove; } } private void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs) { ((UIElement)sender).CaptureMouse(); } private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs) { ((UIElement)sender).ReleaseMouseCapture(); } private void ElementOnMouseMove(object sender, MouseEventArgs mouseEventArgs) { FrameworkElement element = sender as FrameworkElement; Canvas parent = element.FindAncestor<Canvas>(); var mousePos = mouseEventArgs.GetPosition(parent); if (!((UIElement)sender).IsMouseCaptured) return; if (mousePos.X < parent.Width && mousePos.Y < parent.Height && mousePos.X >= 0 && mousePos.Y >=0) ((sender as FrameworkElement).DataContext as Step).Pos = new System.Drawing.Point(Convert.ToInt32(Math.Floor(mousePos.X)), Convert.ToInt32((Math.Floor(mousePos.Y)))); } } And my DataTemplate is now: <DataTemplate> <ContentControl Height="1" Width="1" local:DragBehavior.Drag="True" Style="{StaticResource StepCardContentControl}"/> </DataTemplate> I added the FindAncestor static class in a dedicated file like this: public static class FindAncestorHelper { public static T FindAncestor<T>(this DependencyObject obj) where T : DependencyObject { DependencyObject tmp = VisualTreeHelper.GetParent(obj); while (tmp != null && !(tmp is T)) { tmp = VisualTreeHelper.GetParent(tmp); } return tmp as T; } } (My items are now ContentControls). As the items' positions within the canvas are directly managed with their Pos variable (Canvas.SetLeft and Canvas.SetTop based on Pos (Pos.X and Pos.Y) with Binding), I just update it according to the MousePosition within the Canvas. Also, as suggested in a commentary, I will see if there is something better than the ScrollViewer and Viewbox I'm using.
TextBlock Style to always use Run Tag
In WPF Arabic Mode (FlowDirection="RightToLeft"). When i give a number like -24.7% it will print this as %24.7- Following code will fix the above mentioned issues. <Window.Resources> <Style TargetType="Run"> <Setter Property="FlowDirection" Value="LeftToRight" /> </Style> </Window.Resources> <Grid FlowDirection="RightToLeft" > <Grid HorizontalAlignment="Left" Margin="114,127,0,0" VerticalAlignment="Top" Width="279" Height="97"> <TextBlock x:Name="textBlock" Text="-24.7%" ><Run></Run></TextBlock> </Grid> </Grid> Now i want to put the <run><run> tag to all of my Text Blocks Contents, How can i achieve this, So i don't have to replace all of my TextBlocks in the code. How to do this by creating a Style...?? note: I can't go to the TextAlign=Right solution as i can't edit all the textblockes in the application
Can't say I like your approach, but I don't know Arabic gotchas and your situation, so won't argue about that. You can achieve what you want using attached properties (or blend behaviors). Like this: public static class StrangeAttachedProperty { public static bool GetAddRunByDefault(DependencyObject obj) { return (bool) obj.GetValue(AddRunByDefaultProperty); } public static void SetAddRunByDefault(DependencyObject obj, bool value) { obj.SetValue(AddRunByDefaultProperty, value); } public static readonly DependencyProperty AddRunByDefaultProperty = DependencyProperty.RegisterAttached("AddRunByDefault", typeof (bool), typeof (StrangeAttachedProperty), new PropertyMetadata(AddRunByDefaultChanged)); private static void AddRunByDefaultChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var element = d as TextBlock; if (element != null) { // here is the main point - you can do whatever with your textblock here // for example you can check some conditions and not add runs in some cases element.Inlines.Add(new Run()); } } } And in your resources set this property for all text blocks: <Window.Resources> <Style TargetType="TextBlock"> <Setter Property="local:StrangeAttachedProperty.AddRunByDefault" Value="True" /> </Style> <Style TargetType="Run"> <Setter Property="FlowDirection" Value="LeftToRight" /> </Style> </Window.Resources>
Convert Shape into reusable Geometry in WPF
I am trying to convert a System.Windows.Shapes.Shape object into a System.Windows.Media.Geometry object. With the Geometry object, I am going to render it multiple times with a custom graph control depending on a set of data points. This requires that each instance of the Geometry object has a unique TranslateTransform object. Now, I am approaching the issue in two different ways, but neither seems to be working correctly. My custom control uses the following code in order to draw the geometry: //Create an instance of the geometry the shape uses. Geometry geo = DataPointShape.RenderedGeometry.Clone(); //Apply transformation. TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y); geo.Transform = translation; //Create pen and draw geometry. Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness); dc.DrawGeometry(DataPointShape.Fill, shapePen, geo); I have also tried the following alternate code: //Create an instance of the geometry the shape uses. Geometry geo = DataPointShape.RenderedGeometry; //Apply transformation. TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y); dc.PushTransform(translation); //Create pen and draw geometry. Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness); dc.DrawGeometry(DataPointShape.Fill, shapePen, geo); dc.Pop(); //Undo translation. The difference is that the second snippet doesn't clone or modify the Shape.RenderedGeometry property. Oddly enough, I occasionally can view the geometry used for the data points in the WPF designer. However, the behavior is inconsistent and difficult to figure out how to make the geometry always appear. Also, when I execute my application, the data points never appear with the specified geometry. EDIT: I have figured out how to generate the appearance of the geometry. But this only works in design-mode. Execute these steps: Rebuild project. Go to MainWindow.xaml and click in the custom shape object so that the shape's properties load into Visual Studio's property window. Wait until the property window renders what the shape looks like. Modify the data points collection or properties to see the geometry rendered properly. Here is what I want the control to ultimately look like for now: How can I convert a Shape object to a Geometry object for rendering multiple times? Your help is tremendously appreciated! Let me give the full context of my problem, as well as all necessary code to understanding how my control is set up. Hopefully, this might indicate what problems exist in my method of converting the Shape object to a Geometry object. MainWindow.xaml <Window x:Class="CustomControls.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CustomControls"> <Grid> <local:LineGraph> <local:LineGraph.DataPointShape> <Ellipse Width="10" Height="10" Fill="Red" Stroke="Black" StrokeThickness="1" /> </local:LineGraph.DataPointShape> <local:LineGraph.DataPoints> <local:DataPoint X="10" Y="10"/> <local:DataPoint X="20" Y="20"/> <local:DataPoint X="30" Y="30"/> <local:DataPoint X="40" Y="40"/> </local:LineGraph.DataPoints> </local:LineGraph> </Grid> DataPoint.cs This class just has two DependencyProperties (X & Y) and it gives a notification when any of those properties are changed. This notification is used to trigger a re-render via UIElement.InvalidateVisual(). public class DataPoint : DependencyObject, INotifyPropertyChanged { public static readonly DependencyProperty XProperty = DependencyProperty.Register("XProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged)); public static readonly DependencyProperty YProperty = DependencyProperty.Register("YProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged)); private static void DataPoint_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { DataPoint dp = (DataPoint)sender; dp.RaisePropertyChanged(e.Property.Name); } public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } public double X { get { return (double)GetValue(XProperty); } set { SetValue(XProperty, (double)value); } } public double Y { get { return (double)GetValue(YProperty); } set { SetValue(YProperty, (double)value); } } } LineGraph.cs This is the control. It contains the collection of data points and provides mechanisms for re-rendering the data points (useful for WPF designer). Of particular importance is the logic posted above which is inside of the UIElement.OnRender() method. public class LineGraph : FrameworkElement { public static readonly DependencyProperty DataPointShapeProperty = DependencyProperty.Register("DataPointShapeProperty", typeof(Shape), typeof(LineGraph), new FrameworkPropertyMetadata(default(Shape), FrameworkPropertyMetadataOptions.AffectsRender, DataPointShapeChanged)); public static readonly DependencyProperty DataPointsProperty = DependencyProperty.Register("DataPointsProperty", typeof(ObservableCollection<DataPoint>), typeof(LineGraph), new FrameworkPropertyMetadata(default(ObservableCollection<DataPoint>), FrameworkPropertyMetadataOptions.AffectsRender, DataPointsChanged)); private static void DataPointShapeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { LineGraph g = (LineGraph)sender; g.InvalidateVisual(); } private static void DataPointsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { //Collection referenced set or unset. LineGraph g = (LineGraph)sender; INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged; INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged; if (oldValue != null) oldValue.CollectionChanged -= g.DataPoints_CollectionChanged; if (newValue != null) newValue.CollectionChanged += g.DataPoints_CollectionChanged; //Update the point visuals. g.InvalidateVisual(); } private void DataPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { //Collection changed (added/removed from). if (e.OldItems != null) foreach (INotifyPropertyChanged n in e.OldItems) { n.PropertyChanged -= DataPoint_PropertyChanged; } if (e.NewItems != null) foreach (INotifyPropertyChanged n in e.NewItems) { n.PropertyChanged += DataPoint_PropertyChanged; } InvalidateVisual(); } private void DataPoint_PropertyChanged(object sender, PropertyChangedEventArgs e) { //Re-render the LineGraph when a DataPoint has a property that changes. InvalidateVisual(); } public Shape DataPointShape { get { return (Shape)GetValue(DataPointShapeProperty); } set { SetValue(DataPointShapeProperty, (Shape)value); } } public ObservableCollection<DataPoint> DataPoints { get { return (ObservableCollection<DataPoint>)GetValue(DataPointsProperty); } set { SetValue(DataPointsProperty, (ObservableCollection<DataPoint>)value); } } public LineGraph() { //Provide instance-specific value for data point collection instead of a shared static instance. SetCurrentValue(DataPointsProperty, new ObservableCollection<DataPoint>()); } protected override void OnRender(DrawingContext dc) { if (DataPointShape != null) { Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness); foreach (DataPoint dp in DataPoints) { Geometry geo = DataPointShape.RenderedGeometry.Clone(); TranslateTransform translation = new TranslateTransform(dp.X, dp.Y); geo.Transform = translation; dc.DrawGeometry(DataPointShape.Fill, shapePen, geo); } } } } EDIT 2:In response to this answer by Peter Duniho, I would like to provide the alternate method to lying to Visual Studio in creating a custom control. For creating the custom control execute these steps: Create folder in root of project named Themes Create resource dictionary in Themes folder named Generic.xaml Create a style in the resource dictionary for the control. Apply the style from the control's C# code. Generic.xamlHere is an example of for the SimpleGraph described by Peter. <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CustomControls"> <Style TargetType="local:SimpleGraph" BasedOn="{StaticResource {x:Type ItemsControl}}"> <Style.Resources> <EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5"/> </Style.Resources> <Style.Setters> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <Canvas IsItemsHost="True"/> </ItemsPanelTemplate> </Setter.Value> </Setter> <Setter Property="ItemTemplate"> <Setter.Value> <DataTemplate DataType="{x:Type local:DataPoint}"> <Path Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointFill}" Stroke="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStroke}" StrokeThickness="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStrokeThickness}" Data="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointGeometry}"> <Path.RenderTransform> <TranslateTransform X="{Binding X}" Y="{Binding Y}"/> </Path.RenderTransform> </Path> </DataTemplate> </Setter.Value> </Setter> </Style.Setters> </Style> </ResourceDictionary> Lastly, apply the style like so in the SimpleGraph constructor: public SimpleGraph() { DefaultStyleKey = typeof(SimpleGraph); DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry"); }
I think that you are probably not approaching this in the best way. Based on the code you posted, it seems that you are trying to do manually things that WPF is reasonably good at handling automatically. The main tricky part (at least for me…I'm hardly a WPF expert) is that you appear to want to use an actual Shape object as the template for your graph's data point graphics, and I'm not entirely sure of the best way to allow for that template to be replaced programmatically or declaratively without exposing the underlying transformation mechanic that controls the positioning on the graph. So here's an example that ignores that particular aspect (I will comment on alternatives below), but which I believe otherwise serves your precise needs. First, I create a custom ItemsControl class (in Visual Studio, I do this by lying and telling VS I want to add a UserControl, which gets me a XAML-based item in the project…I immediately replace "UserControl" with "ItemsControl" in both the .xaml and .xaml.cs files): XAML: <ItemsControl x:Class="TestSO28332278SimpleGraphControl.SimpleGraph" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl" mc:Ignorable="d" x:Name="root" d:DesignHeight="300" d:DesignWidth="300"> <ItemsControl.Resources> <EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5" /> </ItemsControl.Resources> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas IsItemsHost="True" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type local:DataPoint}"> <Path Data="{Binding ElementName=root, Path=DataPointGeometry}" Fill="Red" Stroke="Black" StrokeThickness="1"> <Path.RenderTransform> <TranslateTransform X="{Binding X}" Y="{Binding Y}"/> </Path.RenderTransform> </Path> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> C#: public partial class SimpleGraph : ItemsControl { public Geometry DataPointGeometry { get { return (Geometry)GetValue(DataPointShapeProperty); } set { SetValue(DataPointShapeProperty, value); } } public static DependencyProperty DataPointShapeProperty = DependencyProperty.Register( "DataPointGeometry", typeof(Geometry), typeof(SimpleGraph)); public SimpleGraph() { InitializeComponent(); DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry"); } } The key here is that I have an ItemsControl class with a default ItemTemplate that has a single Path object. That object's geometry is bound to the controls DataPointGeometry property, and its RenderTransform is bound to the data item's X and Y values as offsets for a translation transform. A simple Canvas is used for the ItemsPanel, as I just need a place to draw things, without any other layout features. Finally, there is a resource defining a default geometry to use, in case the caller doesn't provide one. And about that caller… Here is a simple example of how one might use the above: <Window x:Class="TestSO28332278SimpleGraphControl.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <PathGeometry x:Key="dataPointGeometry" Figures="M 0.5000,0.0000 L 0.6176,0.3382 0.9755,0.3455 0.6902,0.5618 0.7939,0.9045 0.5000,0.7000 0.2061,0.9045 0.3098,0.5618 0.0245,0.3455 0.3824,0.3382 Z"> <PathGeometry.Transform> <ScaleTransform ScaleX="20" ScaleY="20" /> </PathGeometry.Transform> </PathGeometry> </Window.Resources> <Grid> <Border Margin="3" BorderBrush="Black" BorderThickness="1"> <local:SimpleGraph Width="450" Height="300" DataPointGeometry="{StaticResource dataPointGeometry}"> <local:SimpleGraph.Items> <local:DataPoint X="10" Y="10" /> <local:DataPoint X="25" Y="25" /> <local:DataPoint X="40" Y="40" /> <local:DataPoint X="55" Y="55" /> </local:SimpleGraph.Items> </local:SimpleGraph> </Border> </Grid> </Window> In the above, the only truly interesting thing is that I declare a PathGeometry resource, and then bind that resource to the control's DataPointGeometry property. This allows the program to provide a custom geometry for the graph. WPF handles the rest through implicit data binding and templating. If the values of any of the DataPoint objects change, or the data collection itself is modified, the graph will be updated automatically. Here's what it looks like: I will note that the above example only allows you to specify the geometry. The other shape attributes are hard-coded in the data template. This seems slightly different from what you asked to do. But note that you have a few alternatives here that should address your need without requiring the reintroduction of all the extra manual-binding/updating code in your example: Simply add other properties, bound to the template Path object in a fashion similar to the DataPointGeometry property. E.g. DataPointFill, DataPointStroke, etc. Go ahead and allow the user to specify a Shape object, and then use the properties of that object to populate specific properties bound to the properties of the template object. This is mainly a convenience to the caller; if anything, it's a bit of added complication in the graph control itself. Go whole-hog and allow the user to specify a Shape object, which you then convert to a template by using XamlWriter to create some XAML for the object, add the necessary Transform element to the XAML and wrap it in a DataTemplate declaration (e.g. by loading the XAML as an in-memory DOM to modify the XAML), and then using XamlReader to then load the XAML as a template which you can then assign to the ItemTemplate property. Option #3 seems the most complicated to me. So complicated in fact that I did not bother to prototype an example using it…I did a little research and it seems to me that it should work, but I admit that I did not verify for myself that it does. But it would certainly be the gold standard in terms of absolute flexibility for the caller.
How to use WPF custom control properties in XAML Template?
I've created a custom control that is intended to be used as a button but has properties to specify points for a polygon (to be drawn inside the button) and two colors for the gradient. I've declared all the properties in the code and then written the template in the XAML but it doesn't seem to be working. If I hard-code the values into the XAML it works just fine but nothing seems to be happening if I use the property values through TemplateBinding. Any ideas on how to get this to work? Here's my XAML: <Window x:Class="WPFTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" xmlns:my="clr-namespace:WPFTest"> <StackPanel> <StackPanel.Resources> <Style TargetType="my:GradientButton"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type my:GradientButton}"> <Grid> <Ellipse Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Stroke="{TemplateBinding Foreground}" VerticalAlignment="Top" HorizontalAlignment="Left"> <Ellipse.Fill> <LinearGradientBrush> <GradientStop Color="{TemplateBinding GradientStart}" Offset="0"></GradientStop> <GradientStop Color="{TemplateBinding GradientEnd}" Offset="1"></GradientStop> </LinearGradientBrush> </Ellipse.Fill> </Ellipse> <Polygon Points="{TemplateBinding PlayPoints}" Fill="{TemplateBinding Foreground}" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </StackPanel.Resources> <my:GradientButton Content="Button" Height="50" x:Name="gradientButton1" Width="50" GradientStart="#CCCCCC" GradientEnd="#777777" /> </StackPanel> </Window> And here's the code for the custom control: public class GradientButton : Button { internal static DependencyProperty GradientStartProperty; internal static DependencyProperty GradientEndProperty; internal static DependencyProperty PlayPointsProperty; static GradientButton() { GradientStartProperty = DependencyProperty.Register("GradientStart", typeof(Color), typeof(GradientButton)); GradientEndProperty = DependencyProperty.Register("GradientEnd", typeof(Color), typeof(GradientButton)); PlayPointsProperty = DependencyProperty.Register("PlayPoints", typeof(Point[]), typeof(GradientButton)); } public Color GradientStart { get { return (Color)base.GetValue(GradientStartProperty); } set { SetValue(GradientStartProperty, value); } } public Color GradientEnd { get { return (Color)base.GetValue(GradientEndProperty); } set { SetValue(GradientEndProperty, value); } } public Point[] PlayPoints { get //hardcoded return at the moment to get it to work, but this will change later { System.Collections.Generic.List<Point> result = new System.Collections.Generic.List<Point>(); double left = this.Width / 2.77; double top = this.Height / 4.17; double right = this.Width / 1.43; double middle = this.Height / 2.0; double bottom = this.Height / 1.32; result.Add(new Point(left, top)); result.Add(new Point(right, middle)); result.Add(new Point(left, bottom)); return result.ToArray(); } set { SetValue(PlayPointsProperty, value); } } }
Polygon.Points is of type PointCollection. I don't believe a Point[] fits that type. You will need to either change the type of PlayPoints or use a IValueConverter to make the change it's type.
Try using this instead for your Color bindings: Color="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=GradientStart}" TemplateBinding only works in limited situations where declarations are in elements of the visual tree of a ControlTemplate, which is not the case for your LinearGradientBrush. You can see the same behavior, and use the same fix, inside Trigger Setters in a ControlTemplate.
Silverlight with using of DependencyProperty and ControlTemplate
I'm starting to study Silverlight 3 and Visual Studio 2008. I've been trying to create Windows sidebar gadget with button controls that look like circles (I have couple of "roundish" png images). The behavior, I want, is the following: when the mouse hovers over the image it gets larger a bit. When we click on it, then it goes down and up. When we leave the button's image it becomes normal sized again. Cause I'm going to have couple of such controls I decided to implement custom control: like a button but with image and no content text. My problem is that I'm not able to set my custom properties in my template and style. What am I doing wrong? My teamplate control with three additional properties: namespace SilverlightGadgetDocked { public class ActionButton : Button { /// <summary> /// Gets or sets the image source of the button. /// </summary> public String ImageSource { get { return (String)GetValue(ImageSourceProperty); } set { SetValue(ImageSourceProperty, value); } } /// <summary> /// Gets or sets the ratio that is applied to the button's size /// when the mouse control is over the control. /// </summary> public Double ActiveRatio { get { return (Double)GetValue(ActiveRatioProperty); } set { SetValue(ActiveRatioProperty, value); } } /// <summary> /// Gets or sets the offset - the amount of pixels the button /// is shifted when the the mouse control is over the control. /// </summary> public Double ActiveOffset { get { return (Double)GetValue(ActiveOffsetProperty); } set { SetValue(ActiveOffsetProperty, value); } } public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(String), typeof(ActionButton), new PropertyMetadata(String.Empty)); public static readonly DependencyProperty ActiveRatioProperty = DependencyProperty.Register("ActiveRatio", typeof(Double), typeof(ActionButton), new PropertyMetadata(1.0)); public static readonly DependencyProperty ActiveOffsetProperty = DependencyProperty.Register("ActiveOffset", typeof(Double), typeof(ActionButton), new PropertyMetadata(0)); public ActionButton() { this.DefaultStyleKey = typeof(ActionButton); } } } And XAML with styles: <UserControl x:Class="SilverlightGadgetDocked.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:SilverlightGadgetDocked="clr-namespace:SilverlightGadgetDocked" Width="130" Height="150" SizeChanged="UserControl_SizeChanged" MouseEnter="UserControl_MouseEnter" MouseLeave="UserControl_MouseLeave"> <Canvas> <Canvas.Resources> <Style x:Name="ActionButtonStyle" TargetType="SilverlightGadgetDocked:ActionButton"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="SilverlightGadgetDocked:ActionButton"> <Grid> <Image Source="{TemplateBinding ImageSource}" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="DockedActionButtonStyle" TargetType="SilverlightGadgetDocked:ActionButton" BasedOn="{StaticResource ActionButtonStyle}"> <Setter Property="Canvas.ZIndex" Value="2"/> <Setter Property="Canvas.Top" Value="10"/> <Setter Property="Width" Value="30"/> <Setter Property="Height" Value="30"/> <Setter Property="ActiveRatio" Value="1.15"/> <Setter Property="ActiveOffset" Value="5"/> </Style> <Style x:Key="InfoActionButtonStyle" TargetType="SilverlightGadgetDocked:ActionButton" BasedOn="{StaticResource DockedActionButtonStyle}"> <Setter Property="ImageSource" Value="images/action_button_info.png"/> </Style> <Style x:Key="ReadActionButtonStyle" TargetType="SilverlightGadgetDocked:ActionButton" BasedOn="{StaticResource DockedActionButtonStyle}"> <Setter Property="ImageSource" Value="images/action_button_read.png"/> </Style> <Style x:Key="WriteActionButtonStyle" TargetType="SilverlightGadgetDocked:ActionButton" BasedOn="{StaticResource DockedActionButtonStyle}"> <Setter Property="ImageSource" Value="images/action_button_write.png"/> </Style> </Canvas.Resources> <StackPanel> <Image Source="images/background_docked.png" Stretch="None"/> <TextBlock Foreground="White" MaxWidth="130" HorizontalAlignment="Right" VerticalAlignment="Top" Padding="0,0,5,0" Text="Name" FontSize="13"/> </StackPanel> <SilverlightGadgetDocked:ActionButton Canvas.Left="15" Style="{StaticResource InfoActionButtonStyle}" MouseLeftButtonDown="imgActionInfo_MouseLeftButtonDown"/> <SilverlightGadgetDocked:ActionButton Canvas.Left="45" Style="{StaticResource ReadActionButtonStyle}" MouseLeftButtonDown="imgActionRead_MouseLeftButtonDown"/> <SilverlightGadgetDocked:ActionButton Canvas.Left="75" Style="{StaticResource WriteAtionButtonStyle}" MouseLeftButtonDown="imgActionWrite_MouseLeftButtonDown"/> </Canvas> </UserControl> And Visual Studio reports that "Invalid attribute value ActiveRatio for property Property" in line 27 <Setter Property="ActiveRatio" Value="1.15"/> VERY BIG THANKS!!!
To be honest I can't see anything wrong with the code you've posted. Perhaps an explanation of exactly what causes the error you are seeing might give you some clues you can use. The registration of the Dependancy property is what is important here:- public static readonly DependencyProperty ActiveRatioProperty = DependencyProperty.Register("ActiveRatio", typeof(Double), typeof(ActionButton), new PropertyMetadata(1.0)); This creates and registers an instance of a dependency property against the combination of the string "ActiveRatio" and the Type ActionButton. When Silverlight comes to put the following Xaml into action:- <Style x:Key="Stuff" TargetType="local:ActionButton"> <Setter Property="ActiveRatio" Value="1.15" /> </Style> it combines the type specified in the TargetType attribute of the style with the string in the setters Property attribute to find the dependancy property instance. * It can then use the type indicated by the dependency property to convert the string in the setters Value attribute. Finally it can call SetValue on the FrameworkElement on which the style is set passing the DependencyProperty found and the converted value. Now return the * in the previous paragraph. Its at this point that the code has failed. It is failing to find a dependency property registration for the string "ActiveRatio" and the type ActionButton. I can't tell you why its failing, your code clearly registers this name and the type in the style matches the type passed in the registration. I've even written small repro of your code and it works fine. All I can suggest is that you try a complete Rebuild and then run the code. Assuming what you have posted is fairly complete the only other suggestion I have is such a "clutching at straws" exercise I'm not even going to explain my reason. Try adding this to you ActionButton class:- public static ActionButton() { }