[Avalonia-02]ControlTemplate 안에서 자식요소인 Canvas 문제

안녕하세요. Avalonia 로 고통의 시간을 보내고 있습니다. ^^

문제의 서론:

저는 사용자 정의 컨트롤 A(ContentControl 에서 상속받은 컨트롤)와 또 다른 사용자 정의 컨트롤 B(Shape 에서 상속받은 컨트롤) 을 만들었습니다.

A 컨트롤은 B 컨트롤에 부가적인 내용을 담기 위해서 만들었습니다.

부가적으로, 설명하면 B 컨트롤이 만들어 질때(기하학적 모양이 렌러링 될때) 거기에 부가적인 정보를 특정 컨트롤에 표시하고자 A 컨트롤을 만든 것입니다.

문제의 핵심 사항

A 컨트롤의 스타일은 아래와 같습니다.


<Design.PreviewWith>
<controls:PendingConnection />
</Design.PreviewWith>

<Style Selector="controls|PendingConnection">
    <Setter Property="Template">
        <ControlTemplate TargetType="controls:PendingConnection">
                <Canvas Background="Red">
                    <controls:Connection Source="10,10" Target="50,50" 
                                         StrokeThickness="2" Stroke="Black"  
                                         IsVisible="True" Direction="Forward" Fill="Black" />
                    <Border Background="Yellow"
                            Margin="65,65,15,15"
                            IsVisible="True"
                            Padding="3"
                            BorderThickness="2"
                            BorderBrush="Black"
                            CornerRadius="3">
                        <ContentPresenter/>
                    </Border>
                </Canvas>
        </ControlTemplate>
    </Setter>
</Style>

여기서 ControlTemplate 안에 Canvas 로 자식요소로 B 컨트롤과 기타 다른 컨트롤이 들어가는 형태입니다.

질문 내용:
WPF 에서는 Canvas 가 렌더링이 안되더라도 자식요소들이 렌더링이 됩니다. 하지만 Avalonia 에서는 Canvas 가 렌더링이 안되면 자식요소들이 렌더링이 안됩니다. 이러한 현상은 ControlTemplate 안에 있을때만 발생합니다. MainWindow 에서 동일하게 작성하면 WPF 와는 동일합니다.

이 문제를 해결하는 방법은 1번 방법Canvas 를 Grid 로 교체하는 방법이 있고, 2번 방법 cs 에서 Canvas 의 사이즈를 자식 요소들에 따라 설정해주는 방식이 있는데, 사실 둘다 확신없습니다.

확신이 없는 이유는 1번 방법은 간단하지만, 이러한 컨트롤을 내부적으로 엄청 많이 사용할 예정이라 성능의 문제가 발생할 가능성과 기타 생각지 못한 SideEffects 가능성이 있을 것 같은 생각이 드는 점이 있습니다.

2번 문제는 구현의 복잡함? 이 있을 수 있고, 이렇게 하는게 맞는지에 대한 근본적인 의문이 드는 것입니다. 굳이 이렇게 하면서 Canvas 를 써야 해? 라는 의문 말이죠.

혹시 여러 개발자 선배님들은 어떻게 생각되시나요?
혹시 제가 제공한 정보가 부족하다면, 알려주시면 별도로 깃허브를 추가적으로 올리도록 하겠습니다.

감사합니다.

1개의 좋아요

안녕하세요.

이 문제를 해결했었는데, 제가 달아놓았던 것을 잊고 있었습니다.

Avalonia 같은 경우, 를 사용할때, ControlTemplate 내에서 사용할때와 MainWindow 에서 사용할때 렌더링 차이가 발생하는 것을 확인했습니다.

당연히 렌더링 차이가 발생하지 않느냐고 답할 수도 있겠지만, 저는 이 문제는 이상하다(버그) 라고 생각했습니다.

같은 경우는 별도의 속성을 설정하지 않고 사용하면, 부모의 컨트롤, 자식의 컨트롤 에 따라 의 사이즈가 결정되거나 렌더링이 됩니다.

하지마, ControlTemplate 안에서는 의 속성을 설정하지 않으면 (Width, Height, etc) 렌더링이 되지 않는 문제가 발생합니다. WPF 에서는 렌더링이 됩니다.(아마도…)

여러가지 방법으로 고민을 했었는데, 결국은 새롭게 Canvas 를 제작해야 한다라는 결론(현재까지는)에 도달 했습니다.

아래 코드에서 TemplateLayoutCanvas 를 새롭게 제작한 Canvas 입니다.
MeasureOverride 와 ArrangeOverride 를 재정의해서 사용하여, 자식 컨트롤이 들어가면 렌더링이 되도록 하였습니다.

using System;
using Avalonia;
using Avalonia.Controls;

namespace RelativeLocation
{
// TODO 값을 설정하지 못하지 않을까?
// 추후 수정해야함. 일단, 에러때문에 여기다 나둠.
public interface ILocatable
{
Point Location { get; }
}

// 최대값을 설정해주면 된다.
public class TemplateLayoutCanvas : Canvas
{
    protected override Size MeasureOverride(Size constraint)
    {
        double maxWidth = 0.0;
        double maxHeight = 0.0;

        foreach (var child in Children)
        {
            child.Measure(constraint);

            // 자식 컨트롤이 ILocatable 인터페이스를 구현하는 경우, Location 속성을 사용
            Point location = child is ILocatable locatableChild ? locatableChild.Location : new Point(0, 0);

            double childRight = location.X + child.DesiredSize.Width;
            double childBottom = location.Y + child.DesiredSize.Height;

            maxWidth = Math.Max(maxWidth, childRight);
            maxHeight = Math.Max(maxHeight, childBottom);
        }
        // TODO 화살표 사이즈가 커지거나 화살표 말고 다른 도형으로 대체했을 때는 사이즈를 조정해주거나 해야한다.
        // 사이즈를 자동으로 맞춰줘야하는 루틴이 필요하다.
        // 화살표를 화면에 다 담을려면 사이즈를 좀 확장해줘야 한다. 여기서 사이즈는 선분을 기준으로 잡기때문에 화살표 부분은 다 담을 수 없다.
        maxWidth += 20d;
        maxHeight += 20d;
        
        return new Size(maxWidth, maxHeight);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (var child in Children)
        {
            // ILocatable 인터페이스를 구현하는지 확인
            if (child is ILocatable locatableChild)
            {
                Point location = locatableChild.Location;
                
                //child.Arrange(new Rect(location.X, location.Y, child.DesiredSize.Width+20, child.DesiredSize.Height+20));
                child.Arrange(new Rect(location, child.DesiredSize));
            }
            else
            {
                // ILocatable을 구현하지 않는 경우, 기본 위치나 다른 로직을 사용하여 Arrange를 수행
                // 기본 위치를 (0, 0)으로 설정
                child.Arrange(new Rect(0, 0, child.DesiredSize.Width, child.DesiredSize.Height));
            }
        }

        return finalSize;
    }

}

}

코딩하면서 정리하는 설명자료(막쓴 정리자료) :
https://www.notion.so/Canvas-ControlTemplateCanvas-e7e7c3605919452d9218b937131d4983

Avalonia Grid 에서 레이아웃 부분(직접 Grid.cs 를 보면 됨)

4개의 좋아요

저 위에 질문을 github로 정리해서 공유부탁드려도 될까요?

2개의 좋아요

네.

1개의 좋아요

안녕하세요.

일단 해당 내용은 오픈소스로 개발하고 있는 내용입니다.

한번 참고해주세요. 지금 현재 틈틈히 개발중이라 코드가 지저분하지만, 한번 보시면 이해하실 수 있을 거예요.

3개의 좋아요

감사합니다 :slight_smile:
무슨 차이인가 싶어서 한번 돌려보고싶었거든요!

기존코드

<ControlTemplate TargetType="{x:Type controls:PendingConnection}">
	<Canvas Background="Red">
		<!--초기 설정 확인할 것-->
		<controls:Connection x:Name="PART_Connection" Source="{TemplateBinding SourceAnchor}" 
		                     Target="{TemplateBinding TargetAnchor}"
		                     Direction="{TemplateBinding Direction}"
		                     Stroke="{TemplateBinding SetFillAndStroke}"
		                     StrokeThickness="{TemplateBinding StrokeThickness}"
		                     Fill="{TemplateBinding SetFillAndStroke}"
		/>

image

변경코드

<ControlTemplate TargetType="{x:Type controls:PendingConnection}">
	<controls:TemplateLayoutCanvas x:Name="PART_Canvas" Background="Transparent">
		<!--초기 설정 확인할 것-->
		<controls:Connection x:Name="PART_Connection" Source="{TemplateBinding SourceAnchor}"
		                     Target="{TemplateBinding TargetAnchor}"
		                     Direction="{TemplateBinding Direction}"
		                     Stroke="{TemplateBinding SetFillAndStroke}"
		                     StrokeThickness="{TemplateBinding StrokeThickness}"
		                     Fill="{TemplateBinding SetFillAndStroke}"
		/>

ControlTemplate 에 있는 Canvas와 Window의 있는 Canvas
차이점이 문제였군요.

윈도우의 캔버스

컨텐츠컨트롤의 Template의 Canvas

상위 계층(Window)의 사이즈를 그대로 받아 처리되는 것이기 때문에… WPF도 동일하게 상위 계층(부모) 사이즈에 맞게 변형되는게 맞는거같아요


이 글 쓰고 좀 몇 부분 수정해서 돌려보니깐…정상적으로 잘 되는데…
한번 아래 참고 해보세요!

App.axaml 에 보니깐…
image

밑줄 친 녀석이 누락되어있는데 그래서 안나온건 아닐까요?

2개의 좋아요

실제 비쥬얼트리 에서도 전체 사이즈로 잡혀있습니다!

3개의 좋아요

안녕하세요.

스타일을 넣지 않아서 렌더링이 안되는 그런 쉬운 문제는 아니였습니다.

아무튼, 제가 작성하고 있는 오픈소스도 많은 관심 가져주세요.~~