Convertir une forme en géomésortinge réutilisable dans WPF

J’essaie de convertir un object System.Windows.Shapes.Shape en un object System.Windows.Media.Geometry .

Avec l’object Geometry , je vais le rendre plusieurs fois avec un contrôle de graphe personnalisé en fonction d’un ensemble de points de données. Cela nécessite que chaque instance de l’object Geometry ait un object TranslateTransform unique.

Maintenant, j’aborde la question de deux manières différentes, mais aucune ne semble fonctionner correctement. Mon contrôle personnalisé utilise le code suivant pour dessiner la géomésortinge:

 //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); 

J’ai également essayé le code alternatif suivant:

 //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. 

La différence est que le deuxième extrait ne clone ni ne modifie la propriété Shape.RenderedGeometry .

Curieusement, je peux parfois voir la géomésortinge utilisée pour les points de données dans le concepteur WPF. Cependant, le comportement est incohérent et difficile à comprendre comment faire en sorte que la géomésortinge apparaisse toujours . De plus, lorsque j’exécute mon application, les points de données n’apparaissent jamais avec la géomésortinge spécifiée.

MODIFIER:
J’ai compris comment générer l’apparence de la géomésortinge. Mais cela ne fonctionne qu’en mode conception. Exécutez ces étapes:

  • Reconstruire le projet.
  • Accédez à MainWindow.xaml et cliquez sur l’object de forme personnalisé pour que les propriétés de la forme soient chargées dans la fenêtre de propriétés de Visual Studio. Attendez que la fenêtre de propriété affiche l’aspect de la forme.
  • Modifiez la collection de points de données ou les propriétés pour afficher la géomésortinge correctement rendue.

Voici à quoi je veux que le contrôle ressemble finalement pour l’instant: entrez la description de l'image ici

Comment puis-je convertir un object Shape en un object Geometry pour le rendre plusieurs fois?

Votre aide est extrêmement appréciée!


Laissez-moi vous donner le contexte complet de mon problème, ainsi que tout le code nécessaire pour comprendre comment mon contrôle est configuré. J’espère que cela pourrait indiquer quels problèmes existe dans ma méthode de conversion de l’object Shape object Geometry .

MainWindow.xaml

               

DataPoint.cs
Cette classe ne comporte que deux DependencyProperties (X & Y) et donne une notification lorsque l’une de ces propriétés est modifiée. Cette notification est utilisée pour déclencher un nouveau rendu 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(ssortingng 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
Ceci est le contrôle. Il contient la collection de points de données et fournit des mécanismes pour rétablir le rendu des points de données (utile pour le concepteur WPF). La logique affichée ci-dessus, qui fait partie de la méthode UIElement.OnRender (), revêt une importance particulière.

 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), typeof(LineGraph), new FrameworkPropertyMetadata(default(ObservableCollection), 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 DataPoints { get { return (ObservableCollection)GetValue(DataPointsProperty); } set { SetValue(DataPointsProperty, (ObservableCollection)value); } } public LineGraph() { //Provide instance-specific value for data point collection instead of a shared static instance. SetCurrentValue(DataPointsProperty, new ObservableCollection()); } 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:
En réponse à cette réponse de Peter Duniho , je voudrais fournir la méthode alternative pour mentir à Visual Studio lors de la création d’un contrôle personnalisé. Pour créer le contrôle personnalisé, exécutez ces étapes:

  • Créer un dossier à la racine du projet nommé Themes
  • Créer un dictionnaire de ressources dans le dossier Themes nommé Generic.xaml
  • Créez un style dans le dictionnaire de ressources pour le contrôle.
  • Appliquez le style à partir du code C # du contrôle.

Generic.xaml
Voici un exemple de SimpleGraph décrit par Peter.

                            

Enfin, appliquez le style comme SimpleGraph dans le constructeur SimpleGraph :

 public SimpleGraph() { DefaultStyleKey = typeof(SimpleGraph); DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry"); } 

Je pense que vous ne vous en approchez probablement pas de la meilleure façon. D’après le code que vous avez publié, il semble que vous essayez de faire manuellement les tâches que WPF est raisonnablement doué pour gérer automatiquement.

La principale difficulté (du moins pour moi… je ne suis guère un expert de WPF) est que vous semblez vouloir utiliser un object Shape réel comme modèle pour les graphiques de points de données de votre graphique, et je ne suis pas tout à fait sûr du meilleur moyen de permettre le remplacement de ce modèle par programme ou de manière déclarative sans exposer le mécanisme de transformation sous-jacent qui contrôle le positionnement sur le graphique.

Voici donc un exemple qui ignore cet aspect particulier (je commenterai les alternatives ci-dessous), mais qui, à mon avis, répond à vos besoins précis.

Tout d’abord, je crée une classe ItemsControl personnalisée (dans Visual Studio, je le fais en mentant et en disant à VS que je veux append un UserControl , ce qui me donne un élément basé sur XAML dans le projet… Je remplace immédiatement “UserControl” par “ItemsControl” dans les fichiers .xaml et .xaml.cs):

XAML:

                    

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"); } } 

La clé ici est que j’ai une classe ItemsControl avec un ItemTemplate par défaut qui a un seul object Path . La géomésortinge de cet object est liée à la propriété de contrôles DataPointGeometry et son RenderTransform est lié aux valeurs X et Y de l’élément de données en tant que décalage d’une transformation de traduction.

Un Canvas simple est utilisé pour le ItemsPanel , car il me faut simplement un emplacement pour dessiner des éléments, sans autre fonction de présentation. Enfin, il existe une ressource définissant une géomésortinge par défaut à utiliser, au cas où l’appelant n’en fournirait aucune.

Et à propos de cet appelant…

Voici un exemple simple d’utilisation de ce qui précède:

                      

Dans ce qui précède, la seule chose vraiment intéressante est que je déclare une ressource PathGeometry , puis que je la lie à la propriété DataPointGeometry du contrôle. Cela permet au programme de fournir une géomésortinge personnalisée pour le graphique.

WPF gère le rest via une liaison de données implicite et des modèles. Si les valeurs de l’un des objects DataPoint changent ou si la collection de données elle-même est modifiée, le graphique sera mis à jour automatiquement.

Voici à quoi ça ressemble:

Capture d'écran graphique

Je noterai que l’exemple ci-dessus ne vous permet que de spécifier la géomésortinge. Les autres atsortingbuts de forme sont codés en dur dans le modèle de données. Cela semble légèrement différent de ce que vous avez demandé de faire. Notez cependant que vous avez quelques solutions de rechange qui devraient répondre à vos besoins sans exiger la réintroduction de tout le code supplémentaire de liaison manuelle / mise à jour dans votre exemple:

  1. Ajoutez simplement d’autres propriétés, liées à l’object Path du modèle, de manière similaire à la propriété DataPointGeometry . Par DataPointFill , DataPointStroke , DataPointStroke , etc.

  2. Allez-y et permettez à l’utilisateur de spécifier un object Shape , puis utilisez les propriétés de cet object pour renseigner des propriétés spécifiques liées aux propriétés de l’object modèle. Ceci est principalement une commodité pour l’appelant; si quelque chose, c’est un peu de complication ajoutée dans le contrôle graphique lui-même.

  3. Allez-y à fond et permettez à l’utilisateur de spécifier un object Shape , que vous convertissez ensuite en modèle en utilisant XamlWriter pour créer du XAML pour l’object, ajoutez l’élément Transform nécessaire au XAML et enveloppez-le dans une déclaration DataTemplate (par exemple, chargement du XAML en tant que DOM en mémoire pour modifier le XAML), puis utilisation de XamlReader pour charger le XAML en tant que modèle que vous pouvez ensuite affecter à la propriété ItemTemplate .

L’option n ° 3 me semble la plus compliquée. Tellement compliqué en fait que je n’ai pas pris la peine de prototyper un exemple à l’aide de celui-ci… J’ai fait une petite recherche et il me semble que cela devrait fonctionner, mais j’avoue que je ne l’ai pas vérifié par moi-même. Mais ce serait certainement la règle d’or en termes de flexibilité absolue pour l’appelant.