Avalonia - TextBlock을 영화 자막 처럼 표현할 수 있는 방법 있을까요?

안녕하세요.
영화 자막 처럼 표현하는 방법을 알고 싶습니다.
현재는 Avalonia 11.0.10을 사용하는 중이며, 을 이용하여 그림자 효과를 주어 비슷하게 표현을 해주었습니다.
다만, 아무래도 영화 자막 처럼 검은색 테두리가 뚜렷하게 나오지는 않네요.
방법이 있다면 조언좀 부탁드립니다.

감사합니다~

2개의 좋아요

image

흰색글씨에 검은색 테두리 글씨 말씀하시는건가용?

2개의 좋아요

예압~ 제가 원하는 것 입니다.
없다면야 만들어겠지만…ㅠㅠ 지금 만드는 것도 생각 중이에요 ㅎㅎ

2개의 좋아요

글자가 잘렸네요 DropShadowDirectionEffect을 이용해서 그림자 효과를 표현했습니다

2개의 좋아요

아래 링크를 참고했습니다.

해당 링크의 결과는 테두리가 글자를 덮는 방식이라 다시 구현해봤습니다.

사용법

<local:OutlinedTextBlock HorizontalAlignment="Center"
                         VerticalAlignment="Center"
                         Stroke="Black"
                         StrokeThickness="5"
                         FontFamily="a시네마M"
                         Text="영화 자막같은 테두리"
                         FontSize="30"
                         Foreground="#FFFFFF" />

코드

using System;
using System.Threading;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;

namespace MyProject;

public class OutlinedTextBlock : Control
{
    /// <summary>
    /// Defines the <see cref="Text"/> property.
    /// </summary>
    public static readonly StyledProperty<string> TextProperty =
        AvaloniaProperty.Register<OutlinedTextBlock, string>(nameof(Text));

    /// <summary>
    /// Defines the <see cref="FontFamily"/> property.
    /// </summary>
    public static readonly StyledProperty<FontFamily> FontFamilyProperty =
        TextElement.FontFamilyProperty.AddOwner<OutlinedTextBlock>();

    /// <summary>
    /// Defines the <see cref="FontSize"/> property.
    /// </summary>
    public static readonly StyledProperty<double> FontSizeProperty =
        TextElement.FontSizeProperty.AddOwner<OutlinedTextBlock>();

    /// <summary>
    /// Defines the <see cref="FontStyle"/> property.
    /// </summary>
    public static readonly StyledProperty<FontStyle> FontStyleProperty =
        TextElement.FontStyleProperty.AddOwner<OutlinedTextBlock>();

    /// <summary>
    /// Defines the <see cref="FontWeight"/> property.
    /// </summary>
    public static readonly StyledProperty<FontWeight> FontWeightProperty =
        TextElement.FontWeightProperty.AddOwner<OutlinedTextBlock>();

    /// <summary>
    /// Defines the <see cref="Foreground"/> property.
    /// </summary>
    public static readonly StyledProperty<IBrush> ForegroundProperty =
        TextElement.ForegroundProperty.AddOwner<OutlinedTextBlock>();

    /// <summary>
    /// Defines the <see cref="Stroke"/> property.
    /// </summary>
    public static readonly StyledProperty<IBrush> StrokeProperty =
    AvaloniaProperty.Register<OutlinedTextBlock, IBrush>(nameof(Stroke));

    /// <summary>
    /// Defines the <see cref="StrokeThickness"/> property.
    /// </summary>
    public static readonly StyledProperty<double> StrokeThicknessProperty =
        AvaloniaProperty.Register<OutlinedTextBlock, double>(nameof(StrokeThickness));

    private IPen _strokePen;
    private Geometry _textGeometry;

    static OutlinedTextBlock()
    {
        AffectsGeometry<OutlinedTextBlock>(
           BoundsProperty,
           TextProperty,
           FontSizeProperty,
           FontWeightProperty,
           FontStyleProperty,
           StrokeThicknessProperty);
    }

    /// <summary>
    /// Gets or sets the text.
    /// </summary>
    public string Text
    {
        get => GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    /// <summary>
    /// Gets or sets the text.
    /// </summary>
    public IBrush Stroke
    {
        get => GetValue(StrokeProperty);
        set => SetValue(StrokeProperty, value);
    }

    /// <summary>
    /// Gets or sets the text.
    /// </summary>
    public double StrokeThickness
    {
        get => GetValue(StrokeThicknessProperty);
        set => SetValue(StrokeThicknessProperty, value);
    }

    /// <summary>
    /// Gets or sets the font family used to draw the control's text.
    /// </summary>
    public FontFamily FontFamily
    {
        get => GetValue(FontFamilyProperty);
        set => SetValue(FontFamilyProperty, value);
    }

    /// <summary>
    /// Gets or sets the font family used to draw the control's text.
    /// </summary>
    public IBrush Foreground
    {
        get => GetValue(ForegroundProperty);
        set => SetValue(ForegroundProperty, value);
    }

    /// <summary>
    /// Gets or sets the size of the control's text in points.
    /// </summary>
    public double FontSize
    {
        get => GetValue(FontSizeProperty);
        set => SetValue(FontSizeProperty, value);
    }

    /// <summary>
    /// Gets or sets the font style used to draw the control's text.
    /// </summary>
    public FontStyle FontStyle
    {
        get => GetValue(FontStyleProperty);
        set => SetValue(FontStyleProperty, value);
    }

    /// <summary>
    /// Gets or sets the font weight used to draw the control's text.
    /// </summary>
    public FontWeight FontWeight
    {
        get => GetValue(FontWeightProperty);
        set => SetValue(FontWeightProperty, value);
    }

    private Geometry TextGeometry => _textGeometry ?? CreateTextGeometry();

    public override sealed void Render(DrawingContext context)
    {
        var geometry = TextGeometry;

        if (geometry != null)
        {
            if (_strokePen != null)
            {
                context.DrawGeometry(null, _strokePen, geometry);
            }

            if (Foreground != null)
            {
                context.DrawGeometry(Foreground, null, geometry);
            }
        }
    }

    /// <summary>
    /// Marks a property as affecting the shape's geometry.
    /// </summary>
    /// <param name="properties">The properties.</param>
    /// <remarks>
    /// After a call to this method in a control's static constructor, any change to the
    /// property will cause <see cref="InvalidateGeometry"/> to be called on the element.
    /// </remarks>
    protected static void AffectsGeometry<TOT>(params AvaloniaProperty[] properties)
        where TOT : OutlinedTextBlock
    {
        foreach (var property in properties)
        {
            property.Changed.Subscribe(e =>
            {
                if (e.Sender is TOT textBlock)
                {
                    AffectsGeometryInvalidate(textBlock, e);
                }
            });
        }
    }

    /// <summary>
    /// Creates the shape's defining geometry.
    /// </summary>
    /// <returns>Defining <see cref="Geometry"/> of the shape.</returns>
    protected Geometry CreateTextGeometry()
    {
        if (Text == null)
        {
            return null;
        }

        var formattedText = new FormattedText(Text, Thread.CurrentThread.CurrentUICulture, FlowDirection.LeftToRight,
                          new Typeface(FontFamily, FontStyle, FontWeight, FontStretch.Normal), FontSize, Brushes.Black);
        return formattedText.BuildGeometry(new Point(0, 0));
    }

    /// <summary>
    /// Invalidates the geometry of this shape.
    /// </summary>
    protected void InvalidateGeometry()
    {
        _textGeometry = null;

        InvalidateMeasure();
    }

    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);

        if (change.Property == StrokeProperty || change.Property == StrokeThicknessProperty)
        {
            InvalidateMeasure();

            if (StrokeThickness == 0.0 || Stroke == null)
            {
                _strokePen = null;
            }
            else if (_strokePen?.Thickness != StrokeThickness || _strokePen?.Brush != Stroke)
            {
                _strokePen = new Pen(Stroke, StrokeThickness);
            }
        }
        else if (change.Property == ForegroundProperty)
        {
            InvalidateVisual();
        }
    }

    protected override Size MeasureOverride(Size availableSize)
        => TextGeometry?.Bounds.BottomRight is Point rb ? new Size(rb.X, rb.Y) : default;

    private static void AffectsGeometryInvalidate(OutlinedTextBlock control, AvaloniaPropertyChangedEventArgs e)
    {
        // If the geometry is invalidated when Bounds changes, only invalidate when the Size
        // portion changes.
        if (e.Property == BoundsProperty)
        {
            var oldBounds = (Rect)e.OldValue!;
            var newBounds = (Rect)e.NewValue!;

            if (oldBounds.Size == newBounds.Size)
            {
                return;
            }
        }

        control.InvalidateGeometry();
    }
}
3개의 좋아요

우와 감사합니다.
저도 구현해보는 중이였는데, 저는 마침 안드로이드 스튜디오에서 java로 구현한 샘플이 있더라고요. 그거 작업 중이였어요 ㅎㅎ

1개의 좋아요

조금 전에 위의 답변에서 레이아웃 관련 코드를 조금 수정했습니다. 참고하세요.

1개의 좋아요