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

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

감사합니다~

2 Likes

image

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

2 Likes

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

2 Likes

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

2 Likes

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

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

사용법

<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 Likes

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

1 Like

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

1 Like