I was simplifying the code of the PieChart I'd created once I restarted experimenting on WPF after spending a month on Android API and they've almost similar architecture of WPF. At first, I started with Canvas and then Panel, FrameworkElement, UIElement, DrawingVisual and Visual. After all that little experiment I've decided to use FrameworkElement since it gives me Loaded event and DependencyProperty binding for free. The PieChart was created before I got into FrameworkElement so it was with Panel. Today, I've simplified it to this:
public class PieChart : Panel
{
TextBlock infoBlock;
Run value, percentage;
Path ellipse;
DoubleAnimation ellipseAnim, infoAnim;
ScaleTransform infoScale, ellipseScale;
int total;
public PieChart() {
Margin = new Thickness(10);
value = new Run() { FontSize = 32, FontWeight = FontWeights.Bold, Foreground = Brushes.Gray };
percentage = new Run() { Foreground = Brushes.Blue };
infoScale = new ScaleTransform(0, 0);
ellipseScale = new ScaleTransform(0, 0);
infoBlock = new TextBlock() {
TextAlignment = TextAlignment.Center,
Inlines = { value, new LineBreak(), percentage },
RenderTransform = infoScale
};
ellipse = new Path() {
Fill = Brushes.White,
Data = new EllipseGeometry(),
RenderTransform = ellipseScale
};
ellipseAnim = new DoubleAnimation() {
BeginTime = TimeSpan.FromSeconds(1),
Duration = TimeSpan.FromSeconds(1),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }
};
infoAnim = new DoubleAnimation() {
Duration = TimeSpan.FromSeconds(0.5),
EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseInOut }
};
}
protected override Size ArrangeOverride(Size finalSize) {
var list = ItemsSource.Cast<int>().ToList();
double cx = finalSize.Width / 2;
double cy = finalSize.Height / 2;
double radius, startAngle, sweepAngle, endAngle;
startAngle = sweepAngle = endAngle = 0d;
radius = cx > cy ? cy : cx;
int index = 0;
foreach (PieSlice item in Children.OfType<PieSlice>()) {
var value = list[index];
startAngle += sweepAngle;
sweepAngle = 2 * Math.PI * value / total;
endAngle = startAngle + sweepAngle;
bool isLarge = (double)value / total > 0.5;
item.SetParameters(cx, cy, radius, startAngle, sweepAngle, isLarge);
item.Arrange(new Rect(item.DesiredSize));
index++;
}
var geo = (EllipseGeometry)ellipse.Data;
geo.RadiusX = geo.RadiusY = radius - 100;
geo.Center = new Point(cx, cy);
ellipse.Measure(finalSize);
ellipse.Arrange(new Rect(ellipse.DesiredSize));
ellipseScale.CenterX = cx;
ellipseScale.CenterY = cy;
infoBlock.Measure(finalSize);
infoScale.CenterX = infoBlock.DesiredSize.Width / 2;
infoScale.CenterY = infoBlock.DesiredSize.Height / 2;
infoBlock.Arrange(new Rect(new Point(cx - infoScale.CenterX, cy - infoScale.CenterY), infoBlock.DesiredSize));
return finalSize;
}
protected override void OnMouseEnter(MouseEventArgs e) {
var point = e.GetPosition(this);
var result = VisualTreeHelper.HitTest(this, point);
if(result.VisualHit is PieSlice) {
double val = ((PieSlice)result.VisualHit).value;
value.Text = val.ToString();
percentage.Text = (val / total * 100).ToString("N2") + "%";
animate(1);
}
}
protected override void OnMouseLeave(MouseEventArgs e) => animate(0);
void animate(double scale) {
if (scale == 1) infoAnim.BeginTime = TimeSpan.FromSeconds(1.5);
else infoAnim.BeginTime = TimeSpan.FromSeconds(1);
ellipseAnim.To = infoAnim.To = scale;
ellipse.RenderTransform.BeginAnimation(ScaleTransform.ScaleXProperty, ellipseAnim);
ellipse.RenderTransform.BeginAnimation(ScaleTransform.ScaleYProperty, ellipseAnim);
infoScale.BeginAnimation(ScaleTransform.ScaleXProperty, infoAnim);
infoScale.BeginAnimation(ScaleTransform.ScaleYProperty, infoAnim);
}
public IEnumerable ItemsSource {
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(PieChart), new PropertyMetadata(null, onSourceChanged));
static void onSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var o = d as PieChart;
if (e.OldValue != null) o.Children.Clear();
var items = (IEnumerable)e.NewValue;
Random rand = new();
foreach (int item in items) {
o.total += item;
var color = Color.FromRgb((byte)rand.Next(0, 256), (byte)rand.Next(0, 256), (byte)rand.Next(0, 256));
var slice = new PieSlice(new SolidColorBrush(color), item);
o.Children.Add(slice);
}
o.Children.Add(o.ellipse);
o.Children.Add(o.infoBlock);
}
}
Now, I can easily convert it as a FrameworkElement, all I've to do is:
1) change the first line to public class PieChart : FrameworkElement
2) add a VisualCollection field and initialize that in constructor like this:
VisualCollection Children;
public PieChart() {
Children = new VisualCollection(this);
...
}
3) override these two methods this way:
protected override Visual GetVisualChild(int index) => Children[index];
protected override int VisualChildrenCount => Children.Count;
No other changes is required and it works fine both with Panel and FrameworkElement:

What's the difference? Will there be any performance difference if I choose one over the other?
EDIT
One more question, previously, I'd:
slice.MouseEnter += o.showPopup;
slice.MouseLeave += o.hidePopup;
in onSourceChanged so the info changed when I move mouse from one slice to the other. Now it doesn't unless there's a gap or I take mouse away from this. Wanted to use Preview event where I can check e.Source BUT looks like there is no Preview for MouseEnter/Leave. Is there any other PreviewEvent which I can use to do that?