File: System\Drawing\StringFormat.cs
Web Access
Project: src\src\System.Drawing.Common\src\System.Drawing.Common.csproj (System.Drawing.Common)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Drawing.Text;
 
namespace System.Drawing;
 
/// <summary>
///  Encapsulates text layout information (such as alignment and linespacing), display manipulations (such as
///  ellipsis insertion and national digit substitution) and OpenType features.
/// </summary>
public sealed unsafe class StringFormat : MarshalByRefObject, ICloneable, IDisposable
{
    internal GpStringFormat* _nativeFormat;
 
    private StringFormat(GpStringFormat* format) => _nativeFormat = format;
 
    /// <summary>
    ///  Initializes a new instance of the <see cref='StringFormat'/> class.
    /// </summary>
    public StringFormat() : this(0, 0)
    {
    }
 
    /// <summary>
    ///  Initializes a new instance of the <see cref='StringFormat'/> class with the specified <see cref='StringFormatFlags'/>.
    /// </summary>
    public StringFormat(StringFormatFlags options) : this(options, 0)
    {
    }
 
    /// <summary>
    ///  Initializes a new instance of the <see cref='StringFormat'/> class with the specified
    ///  <see cref='StringFormatFlags'/> and language.
    /// </summary>
    public StringFormat(StringFormatFlags options, int language)
    {
        GpStringFormat* format;
        PInvoke.GdipCreateStringFormat((int)options, (ushort)language, &format).ThrowIfFailed();
        _nativeFormat = format;
    }
 
    /// <summary>
    ///  Initializes a new instance of the <see cref='StringFormat'/> class from the specified
    ///  existing <see cref='StringFormat'/>.
    /// </summary>
    public StringFormat(StringFormat format)
    {
        ArgumentNullException.ThrowIfNull(format);
        GpStringFormat* newFormat;
        PInvoke.GdipCloneStringFormat(format._nativeFormat, &newFormat).ThrowIfFailed();
        _nativeFormat = newFormat;
        GC.KeepAlive(format);
    }
 
    /// <summary>
    ///  Cleans up Windows resources for this <see cref='StringFormat'/>.
    /// </summary>
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
 
    private void Dispose(bool disposing)
    {
        if (_nativeFormat is not null)
        {
            try
            {
#if DEBUG
                Status status = !Gdip.Initialized ? Status.Ok :
#endif
                PInvoke.GdipDeleteStringFormat(_nativeFormat);
#if DEBUG
                Debug.Assert(status == Status.Ok, $"GDI+ returned an error status: {status}");
#endif
            }
            catch (Exception ex)
            {
                if (ClientUtils.IsCriticalException(ex))
                {
                    throw;
                }
 
                Debug.Fail($"Exception thrown during Dispose: {ex}");
            }
            finally
            {
                _nativeFormat = null;
            }
        }
    }
 
    /// <summary>
    ///  Creates an exact copy of this <see cref='StringFormat'/>.
    /// </summary>
    public object Clone() => new StringFormat(this);
 
    /// <summary>
    ///  Gets or sets a <see cref='StringFormatFlags'/> that contains formatting information.
    /// </summary>
    public StringFormatFlags FormatFlags
    {
        get
        {
            StringFormatFlags format;
            PInvoke.GdipGetStringFormatFlags(_nativeFormat, (int*)&format).ThrowIfFailed();
            GC.KeepAlive(this);
            return format;
        }
        set
        {
            PInvoke.GdipSetStringFormatFlags(_nativeFormat, (int)value).ThrowIfFailed();
            GC.KeepAlive(this);
        }
    }
 
    /// <summary>
    ///  Sets the measure of characters to the specified range.
    /// </summary>
    public void SetMeasurableCharacterRanges(CharacterRange[] ranges)
    {
        ArgumentNullException.ThrowIfNull(ranges);
 
        // Passing no count will clear the ranges, but it still requires a valid pointer. Taking the address of an
        // empty array gives a null pointer, so we need to pass a dummy value.
        GdiPlus.CharacterRange stub;
        fixed (void* r = ranges)
        {
            PInvoke.GdipSetStringFormatMeasurableCharacterRanges(
                _nativeFormat,
                ranges.Length,
                r is null ? &stub : (GdiPlus.CharacterRange*)r).ThrowIfFailed();
        }
 
        GC.KeepAlive(this);
    }
 
    /// <summary>
    ///  Specifies text alignment information.
    /// </summary>
    public StringAlignment Alignment
    {
        get
        {
            StringAlignment alignment;
            PInvoke.GdipGetStringFormatAlign(_nativeFormat, (GdiPlus.StringAlignment*)&alignment).ThrowIfFailed();
            GC.KeepAlive(this);
            return alignment;
        }
        set
        {
            if (value is < StringAlignment.Near or > StringAlignment.Far)
            {
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(StringAlignment));
            }
 
            PInvoke.GdipSetStringFormatAlign(_nativeFormat, (GdiPlus.StringAlignment)value).ThrowIfFailed();
            GC.KeepAlive(this);
        }
    }
 
    /// <summary>
    ///  Gets or sets the line alignment.
    /// </summary>
    public StringAlignment LineAlignment
    {
        get
        {
            StringAlignment alignment;
            PInvoke.GdipGetStringFormatLineAlign(_nativeFormat, (GdiPlus.StringAlignment*)&alignment).ThrowIfFailed();
            GC.KeepAlive(this);
            return alignment;
        }
        set
        {
            if (value is < 0 or > StringAlignment.Far)
            {
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(StringAlignment));
            }
 
            PInvoke.GdipSetStringFormatLineAlign(_nativeFormat, (GdiPlus.StringAlignment)value).ThrowIfFailed();
            GC.KeepAlive(this);
        }
    }
 
    /// <summary>
    ///  Gets or sets the <see cref='HotkeyPrefix'/> for this <see cref='StringFormat'/> .
    /// </summary>
    public HotkeyPrefix HotkeyPrefix
    {
        get
        {
            HotkeyPrefix hotkeyPrefix;
            PInvoke.GdipGetStringFormatHotkeyPrefix(_nativeFormat, (int*)&hotkeyPrefix).ThrowIfFailed();
            GC.KeepAlive(this);
            return hotkeyPrefix;
        }
        set
        {
            if (value is < HotkeyPrefix.None or > HotkeyPrefix.Hide)
            {
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(HotkeyPrefix));
            }
 
            PInvoke.GdipSetStringFormatHotkeyPrefix(_nativeFormat, (int)value).ThrowIfFailed();
            GC.KeepAlive(this);
        }
    }
 
    /// <summary>
    ///  Sets tab stops for this <see cref='StringFormat'/>.
    /// </summary>
    public void SetTabStops(float firstTabOffset, float[] tabStops)
    {
        ArgumentNullException.ThrowIfNull(tabStops);
 
        if (firstTabOffset < 0)
        {
            throw new ArgumentException(SR.Format(SR.InvalidArgumentValue, nameof(firstTabOffset), firstTabOffset));
        }
 
        // To clear the tab stops you need to pass a count of 0 with a valid pointer. Taking the address of an
        // empty array gives a null pointer, so we need to pass a dummy value.
        float stub;
        fixed (float* ts = tabStops)
        {
            PInvoke.GdipSetStringFormatTabStops(
                _nativeFormat,
                firstTabOffset,
                tabStops.Length,
                ts is null ? &stub : ts).ThrowIfFailed();
            GC.KeepAlive(this);
        }
    }
 
    /// <summary>
    ///  Gets the tab stops for this <see cref='StringFormat'/>.
    /// </summary>
    public float[] GetTabStops(out float firstTabOffset)
    {
        int count;
        PInvoke.GdipGetStringFormatTabStopCount(_nativeFormat, &count).ThrowIfFailed();
 
        if (count == 0)
        {
            firstTabOffset = 0;
            return [];
        }
 
        float[] tabStops = new float[count];
 
        fixed (float* fto = &firstTabOffset)
        fixed (float* ts = tabStops)
        {
            PInvoke.GdipGetStringFormatTabStops(_nativeFormat, count, fto, ts).ThrowIfFailed();
            GC.KeepAlive(this);
            return tabStops;
        }
    }
 
    // String trimming. How to handle more text than can be displayed
    // in the limits available.
 
    /// <summary>
    ///  Gets or sets the <see cref='StringTrimming'/> for this <see cref='StringFormat'/>.
    /// </summary>
    public StringTrimming Trimming
    {
        get
        {
            StringTrimming trimming;
            PInvoke.GdipGetStringFormatTrimming(_nativeFormat, (GdiPlus.StringTrimming*)&trimming).ThrowIfFailed();
            GC.KeepAlive(this);
            return trimming;
        }
        set
        {
            if (value is < StringTrimming.None or > StringTrimming.EllipsisPath)
            {
                throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(StringTrimming));
            }
 
            PInvoke.GdipSetStringFormatTrimming(_nativeFormat, (GdiPlus.StringTrimming)value).ThrowIfFailed();
            GC.KeepAlive(this);
        }
    }
 
    /// <summary>
    ///  Gets a generic default <see cref='StringFormat'/>.
    /// </summary>
    /// <devdoc>
    ///  Remarks from MSDN: A generic, default StringFormat object has the following characteristics:
    ///
    ///     - No string format flags are set.
    ///     - Character alignment and line alignment are set to StringAlignmentNear.
    ///     - Language ID is set to neutral language, which means that the current language associated
    ///       with the calling thread is used.
    ///     - String digit substitution is set to StringDigitSubstituteUser.
    ///     - Hot key prefix is set to HotkeyPrefixNone.
    ///     - Number of tab stops is set to zero.
    ///     - String trimming is set to StringTrimmingCharacter.
    /// </devdoc>
    public static StringFormat GenericDefault
    {
        get
        {
            GpStringFormat* format;
            PInvoke.GdipStringFormatGetGenericDefault(&format).ThrowIfFailed();
            return new StringFormat(format);
        }
    }
 
    /// <summary>
    ///  Gets a generic typographic <see cref='StringFormat'/>.
    /// </summary>
    /// <devdoc>
    ///  Remarks from MSDN: A generic, typographic StringFormat object has the following characteristics:
    ///
    ///     - String format flags StringFormatFlagsLineLimit, StringFormatFlagsNoClip,
    ///       and StringFormatFlagsNoFitBlackBox are set.
    ///     - Character alignment and line alignment are set to StringAlignmentNear.
    ///     - Language ID is set to neutral language, which means that the current language associated
    ///       with the calling thread is used.
    ///     - String digit substitution is set to StringDigitSubstituteUser.
    ///     - Hot key prefix is set to HotkeyPrefixNone.
    ///     - Number of tab stops is set to zero.
    ///     - String trimming is set to StringTrimmingNone.
    /// </devdoc>
    public static StringFormat GenericTypographic
    {
        get
        {
            GpStringFormat* format;
            PInvoke.GdipStringFormatGetGenericTypographic(&format).ThrowIfFailed();
            return new StringFormat(format);
        }
    }
 
    public void SetDigitSubstitution(int language, StringDigitSubstitute substitute)
    {
        PInvoke.GdipSetStringFormatDigitSubstitution(
            _nativeFormat,
            (ushort)language,
            (GdiPlus.StringDigitSubstitute)substitute).ThrowIfFailed();
 
        GC.KeepAlive(this);
    }
 
    /// <summary>
    ///  Gets the <see cref='StringDigitSubstitute'/> for this <see cref='StringFormat'/>.
    /// </summary>
    public StringDigitSubstitute DigitSubstitutionMethod
    {
        get
        {
            StringDigitSubstitute digitSubstitute;
            PInvoke.GdipGetStringFormatDigitSubstitution(
                _nativeFormat,
                null,
                (GdiPlus.StringDigitSubstitute*)&digitSubstitute).ThrowIfFailed();
 
            GC.KeepAlive(this);
            return digitSubstitute;
        }
    }
 
    /// <summary>
    ///  Gets the language of <see cref='StringDigitSubstitute'/> for this <see cref='StringFormat'/>.
    /// </summary>
    public int DigitSubstitutionLanguage
    {
        get
        {
            ushort language;
            PInvoke.GdipGetStringFormatDigitSubstitution(_nativeFormat, &language, null).ThrowIfFailed();
            GC.KeepAlive(this);
            return language;
        }
    }
 
    internal int GetMeasurableCharacterRangeCount()
    {
        int count;
        PInvoke.GdipGetStringFormatMeasurableCharacterRangeCount(_nativeFormat, &count).ThrowIfFailed();
        GC.KeepAlive(this);
        return count;
    }
 
    /// <summary>
    ///  Cleans up Windows resources for this <see cref='StringFormat'/>.
    /// </summary>
    ~StringFormat() => Dispose(disposing: false);
 
    /// <summary>
    ///  Converts this <see cref='StringFormat'/> to a human-readable string.
    /// </summary>
    public override string ToString() => $"[StringFormat, FormatFlags={FormatFlags}]";
}