lachfoy
Topic Author
Posts: 15
Joined: 20 Feb 2024, 06:25

Drawing complex controls

12 Jun 2024, 03:18

Hi,

I'm working on a fairly complex custom control for displaying data on a radial chart (similar to what can be found here https://www.syncfusion.com/wpf-controls ... adar-chart).
I need to construct a path geometry that shows the data points, as well as add labels around the chart.

My initial thought was to build a path object inside a canvas, then add new textblocks as children and position them with the canvas left and top attached properties. Is this a good approach?
Would it ever be recommended to just override the OnRender instead?

Thanks
 
User avatar
jsantos
Site Admin
Posts: 4822
Joined: 20 Jan 2012, 17:18
Contact:

Re: Drawing complex controls

14 Jun 2024, 20:40

Your approach is good, I understand you will have a UserControl with a XAML, and the different dependency properties of the user control will change the aspect.

But I think, moving this to small pieces, like different custom controls for rendering every part using OnRender and then having a master control orchestrating everything is a better approach and also more compatible with having different styles.

Please, let us know if you need help with this.
 
User avatar
jsantos
Site Admin
Posts: 4822
Joined: 20 Jan 2012, 17:18
Contact:

Re: Drawing complex controls

14 Jun 2024, 20:50

For example, this is a RadialGauge control we created to be used from Noesis Studio (if you need the source code of this, please let me know, it is still work in progress).

The default template is like this:
<ControlTemplate TargetType="noesis:RadialGauge">
  <Grid>
    <Viewbox>
      <Grid Width="200" Height="200">
        <noesis:RadialBorder
            MinAngle="{TemplateBinding MinAngle}" MaxAngle="{TemplateBinding MaxAngle}"
            Minimum="{TemplateBinding Minimum}" Maximum="{TemplateBinding Maximum}"
            Start="{TemplateBinding Minimum}"
            End="{TemplateBinding Maximum}"
            BorderWidth="{TemplateBinding ScaleWidth}"
            Padding="{TemplateBinding ScalePadding}"
            Brush="{TemplateBinding ScaleBrush}"/>
        <noesis:RadialBorder
            MinAngle="{TemplateBinding MinAngle}" MaxAngle="{TemplateBinding MaxAngle}"
            Minimum="{TemplateBinding Minimum}" Maximum="{TemplateBinding Maximum}"
            Start="{TemplateBinding Minimum}"
            End="{TemplateBinding Value}"
            BorderWidth="{TemplateBinding ScaleWidth}"
            Padding="{TemplateBinding ScalePadding}"
            Brush="{TemplateBinding TrailBrush}"/>
        <noesis:RadialTicks
            MinAngle="{TemplateBinding MinAngle}" MaxAngle="{TemplateBinding MaxAngle}"
            Minimum="{TemplateBinding Minimum}" Maximum="{TemplateBinding Maximum}" 
            Brush="{TemplateBinding TickBrush}"
            Spacing="{TemplateBinding TickSpacing}"
            Padding="{TemplateBinding TickPadding}"
            TickWidth="{TemplateBinding TickWidth}"
            TickLength="{TemplateBinding TickLength}"/>
        <noesis:RadialNeedle
          MinAngle="{TemplateBinding MinAngle}" MaxAngle="{TemplateBinding MaxAngle}"
          Minimum="{TemplateBinding Minimum}" Maximum="{TemplateBinding Maximum}"
          Value="{TemplateBinding Value}">
          <Rectangle
              Fill="{TemplateBinding NeedleBrush}"
              Width="{TemplateBinding NeedleWidth}"
              Height="{TemplateBinding NeedleLength}"
              VerticalAlignment="Top"/>
        </noesis:RadialNeedle>
      </Grid>
    </Viewbox>
  </Grid>
</ControlTemplate>
For example, the RadialTicks control used in the above template is implemented deriving from FrameworkElement and with this OnRender method:
void RadialTicks::OnRender(Noesis::DrawingContext* context)
{
    Brush* brush = GetBrush();

    float minimum = GetMinimum();
    float maximum = GetMaximum();
    float width = GetTickWidth();
    float height = GetTickLength();
    float padding = GetPadding();
    float spacing = Max(GetSpacing(), fabsf(minimum - maximum) * 0.001f);
    float minAngle = GetMinAngle();
    float maxAngle = GetMaxAngle();

    Ptr<Pen> pen = MakePtr<Pen>();
    pen->SetBrush(brush);
    pen->SetThickness(width);

    Size area = GetRenderSize();
    float orgX = area.width * 0.5f;
    float orgY = area.height * 0.5f;

    for (float i = minimum; i <= maximum; i += spacing)
    {
        float angle = ValueToAngle(i, minimum, maximum, minAngle, maxAngle);

        Point p0(orgX, padding);
        Point p1(orgX, padding + height);

        Transform2 mtx = Transform2::Identity();
        mtx.RotateAt(angle * DegToRad, orgX, orgY);

        context->DrawLine(pen, p0 * mtx, p1 * mtx);
    }
}
I hope this helps!
 
lachfoy
Topic Author
Posts: 15
Joined: 20 Feb 2024, 06:25

Re: Drawing complex controls

17 Jun 2024, 07:27

Hi, thanks. I like this approach.
The code you've provided is very helpful.

Although, I'm stuck on using the OnRender with text. I don't quite understand how to use FormattedText to draw labels on each point.
For context, here is my OnRender function so far. :
void RadarSeries::OnRender(DrawingContext* drawingContext)
{
	Brush* fill = GetFill();

	Brush* stroke = GetStroke();
	float strokeThickness = GetStrokeThickness();

	Ptr<Pen> pen = *new Pen();
	pen->SetBrush(stroke);
	pen->SetThickness(strokeThickness);

	Size area = GetRenderSize();
	float radius = Math::min(area.width, area.height) * 0.5f;
	float centerX = area.width * 0.5f;
	float centerY = area.height * 0.5f;
	int numSides = 6; // Items.Count

	Ptr<PathGeometry> geo = *new PathGeometry();
	Ptr<PathFigure> figure = *new PathFigure();

	figure->SetIsClosed(true);

	float angleStep = (float)2PI / numSides;
	float angle = 0.0f;

	float maximum = GetMaximum();

	ItemCollection* items = GetItems();

	{
		Ptr<BaseComponent> item = items->GetItemAt(0);
		const TypeClass* type = item->GetClassType();

		const TypeProperty* prop = type->FindProperty(Symbol(GetValuePath()));

		float value;
		prop->Get(item.GetPtr(), &value);
		
		float fillRadius = value / maximum * radius;

		figure->SetStartPoint(Point(centerX + fillRadius * cosf(angle), centerY + fillRadius * sinf(angle)));
		angle += angleStep;
	}

	for (int i = 1; i < numSides; i++)
	{
		Ptr<BaseComponent> item = items->GetItemAt(i);
		const TypeClass* type = item->GetClassType();

		const TypeProperty* prop = type->FindProperty(Symbol(GetValuePath()));

		float value;
		prop->Get(item.GetPtr(), &value);

		float fillRadius = value / maximum * radius;

		Ptr<LineSegment> segment =
			*new LineSegment(Point(centerX + fillRadius * cosf(angle), centerY + fillRadius * sinf(angle)), true);
		figure->GetSegments()->Add(segment);

		angle += angleStep;
	}

	geo->GetFigures()->Add(figure);
	drawingContext->DrawGeometry(fill, pen, geo);
}
 
User avatar
jsantos
Site Admin
Posts: 4822
Joined: 20 Jan 2012, 17:18
Contact:

Re: Drawing complex controls

17 Jun 2024, 13:45

This is the OnRender source code for RadialNumbers class.
void RadialNumbers::OnRender(Noesis::DrawingContext* context)
{
    FontFamily* font = GetFontFamily();
    float minimum = GetMinimum();
    float maximum = GetMaximum();
    float padding = GetPadding();
    float spacing = Max(GetSpacing(), fabsf(minimum - maximum) * 0.001f);
    float fontSize = GetFontSize();
    float minAngle = GetMinAngle();
    float maxAngle = GetMaxAngle();
    bool rotated = GetRotated();
    Brush* brush = GetBrush();

    Size area = GetRenderSize();
    float orgX = area.width * 0.5f;
    float orgY = area.height * 0.5f;

    for (float i = minimum; i <= maximum; i += spacing)
    {
        float angle = ValueToAngle(i, minimum, maximum, minAngle, maxAngle);

        String str;
        StringFormat(i, str);

        Ptr<FormattedText> text = MakePtr<FormattedText>(str.Str(), font, fontSize, brush);
        Rect rect = text->GetBounds();

        if (rotated)
        {
            Transform2 mtx = Transform2::Identity();
            mtx = PreTrans(orgX - rect.width * 0.5f, padding - rect.height * 0.5f, mtx);
            mtx.RotateAt(angle * DegToRad, orgX, orgY);

            context->PushTransform(MakePtr<MatrixTransform>(mtx));
            context->DrawText(text, rect);
            context->Pop();
        }
        else
        {
            Transform2 mtx = Transform2::Identity();
            mtx.RotateAt(angle * DegToRad, orgX, orgY);

            Point o = Point(orgX, padding) * mtx;
            rect.x = o.x - rect.width * 0.5f;
            rect.y = o.y - rect.height * 0.5f;

            context->DrawText(text, rect);
        }
    }
}
 
lachfoy
Topic Author
Posts: 15
Joined: 20 Feb 2024, 06:25

Re: Drawing complex controls

18 Jun 2024, 02:30

Hey, thanks for sharing.
Looks like the version I'm on (3.17) doesn't have any parameters to the constructor of FormattedText.
I was able to reference this thread to get text drawing viewtopic.php?p=13503&hilit=FormattedText#p13503
But I'm not too sure about my usage of calls to Measure and Layout. Calling Measure was the only way I was able to get the bounds of the text first before calling Layout.
float labelAngle = 0;
for (int i = 0; i < numSides; i++)
{
	Ptr<FormattedText> text = *new FormattedText();
		
	text->BuildTextRuns(
		"Label",
		nullptr,
		font,
		FontWeight_Normal,
		FontStretch_Normal,
		FontStyle_Normal,
		fontSize,
		0.0f,
		nullptr,
		foreground,
		nullptr,
		TextDecorations_None,
		1);

	text->Measure(TextAlignment_Left, TextWrapping_Wrap, TextTrimming_None, FLT_MAX, FLT_MAX, 0.0f, LineStackingStrategy_MaxHeight);
	Rect rect = text->GetBounds();

	text->Layout(
		TextAlignment_Left, TextWrapping_Wrap, TextTrimming_None, rect.width, rect.height, 0.0f, LineStackingStrategy_MaxHeight);

	Point p = Point(centerX + radius * cosf(labelAngle), centerY + radius * sinf(labelAngle));

	rect.x = p.x - rect.width * 0.5f;
	rect.y = p.y - rect.height * 0.5f;
	drawingContext->DrawText(text, rect);

	labelAngle += angleStep;
}
 
User avatar
jsantos
Site Admin
Posts: 4822
Joined: 20 Jan 2012, 17:18
Contact:

Re: Drawing complex controls

21 Jun 2024, 10:51

This is what the constructor of FormattedText is calling in 3.2.X
void FormattedText::CalculateMetrics(const char* text, FontFamily* fontFamily,
    FontWeight weight, FontStretch stretch, FontStyle style,
    float fontSize, Brush* foreground, FlowDirection flowDirection,
    float maxWidth, float maxHeight, float lineHeight,
    TextAlignment textAlignment, TextTrimming textTrimming)
{
    BuildTextRuns(text, nullptr, fontFamily, weight, stretch, style, fontSize, 0.0f, nullptr,
        foreground, nullptr, flowDirection, TextDecorations_None, 0);

    TextWrapping textWrapping = textTrimming == TextTrimming_None ?
        TextWrapping_Wrap : TextWrapping_WrapWithOverflow;

    Size sz = Measure(textAlignment, textWrapping, textTrimming, maxWidth, maxHeight, lineHeight,
        LineStackingStrategy_MaxHeight, flowDirection);

    Layout(textAlignment, textWrapping, textTrimming, sz.width, sz.height, Thickness(), lineHeight,
        LineStackingStrategy_MaxHeight, flowDirection, false);
}
Please, let me know if this helps.
 
lachfoy
Topic Author
Posts: 15
Joined: 20 Feb 2024, 06:25

Re: Drawing complex controls

25 Jun 2024, 00:59

This does help, thank you!
I really appreciate the support.

Who is online

Users browsing this forum: No registered users and 2 guests