Выравнивание строки вручную для метода DrawString() в С#

Я реализовал довольно рудиментарный метод «Justify» для рисования строки, однако я хотел бы оптимизировать его, чтобы расстояние было немного более рассредоточенным.

Что у меня есть до сих пор, это:

string lastword = line.Split(' ').Last();
string lineNoLastWord = line.Substring(0,line.LastIndexOf(" ")).Trim();;
g.DrawString( lineNoLastWord, Font, brush, textBounds, sf );
g.DrawString( lastword, Font, brush, textBounds, ConvertAlignment( System.Windows.TextAlignment.Right ) );

ConvertAlignment — это настраиваемый метод следующим образом:

private StringFormat ConvertAlignment(System.Windows.TextAlignment align) {
    StringFormat s = new StringFormat();
    switch ( align ) {
        case System.Windows.TextAlignment.Left:
        case System.Windows.TextAlignment.Justify:
            s.LineAlignment=StringAlignment.Near;
            break;
        case System.Windows.TextAlignment.Right:
            s.LineAlignment=StringAlignment.Far;
            break;
        case System.Windows.TextAlignment.Center:
            s.LineAlignment=StringAlignment.Center;
            break;
    }
    s.Alignment = s.LineAlignment;
    return s;
}

Результат близок, но требует некоторой корректировки пробелов в строке lineNoLastWord.

Еще немного предыстории кода. line является результатом метода, который отвечает за обнаружение того, выходит ли строка за границы (ширины), и разбивает ее на строки и слова, разбивая и измеряя по мере ее продвижения, чтобы убедиться, что вся строка остается в пределах ширины области для быть нарисованы. Метод реализует другие свойства в гораздо большем классе, но вот его суть:

internal LineBreaker breakIntoLines( string s, int maxLineWidth ) {
    List<string> sResults = new List<string>();

    int stringHeight;
    int lineHeight;
    int maxWidthPixels = maxLineWidth;

    string[] lines = s.Split(new string[] { "\n", "\r\n" }, StringSplitOptions.None);
    using ( Graphics g=Graphics.FromImage( Pages[CurrentPage - 1] ) ) {
        g.CompositingQuality = CompositingQuality.HighQuality;
        if ( maxLineWidth<=0||maxLineWidth>( Pages[CurrentPage-1].Width-X ) ) {
            maxWidthPixels=Pages[CurrentPage-1].Width-X;
        }
        lineHeight = (Int32)( g.MeasureString( "X", Font ).Height*(float)( (float)LineSpacing/(float)100 ) );
        stringHeight = (Int32)g.MeasureString( "X", Font ).Height;
        foreach ( string line in lines ) {
            string[] words=line.Split( new string[] { " " }, StringSplitOptions.None );
            sResults.Add( "" );
            for ( int i=0; i<words.Length; i++ ) {
                if ( sResults[sResults.Count-1].Length==0 ) {
                    sResults[sResults.Count-1]=words[i];
                } else {
                    if ( g.MeasureString( sResults[sResults.Count-1]+" "+words[i], Font ).Width<maxWidthPixels ) {
                        sResults[sResults.Count-1]+=" "+words[i];
                    } else {
                        sResults.Add( words[i] );
                    }
                }
            }
        }
    }
    return new LineBreaker() {
        LineHeight = lineHeight,
        StringHeight = stringHeight,
        MaxWidthPixels = maxWidthPixels,
        Lines = sResults
    };
}

internal class LineBreaker {
    public List<string> Lines { get; set; }
    public int MaxWidthPixels { get; set; }
    public int StringHeight { get; set; }
    public int LineHeight { get; set; }

    public LineBreaker() {
        Lines = new List<string>();
        MaxWidthPixels = 0;
        StringHeight = 0;
        LineHeight = 0;
    }

    public LineBreaker( List<string> lines, int maxWidthPixels, int stringHeight, int lineHeight ) {
        Lines = lines;
        MaxWidthPixels = maxWidthPixels;
        LineHeight = lineHeight;
        StringHeight = stringHeight;
    }
}

На следующем изображении показана проблема, которую это вызывает:

демонстрация

Я также видел этот вопрос и ответы на stackoverflow и обнаружил, что он также является неэффективным способом пробела из-за неизвестного размера строки, а неизвестная ширина документа приведет к тому, что строка будет слишком длинной, если слишком много слов, или слишком короткой без правильного обоснования. Полное выравнивание означает, что текст выровнен как по левой, так и по правой стороне, и, как правило, содержимое внутри находится на максимально возможном равномерном расстоянии друг от друга. Вот как я хотел бы это реализовать.

Решение, скорее всего, заключается в вычислении строк lastWord и lineNoLastWord с некоторыми измерениями, чтобы гарантировать жизнеспособность вывода в том смысле, что никакие два слова в строке не будут совпадать или объединяться, и не будет отступов. правая сторона, однако левая, может по-прежнему содержать отступ или вкладку. Другая часть, которую следует учитывать, заключается в том, что если строка короче определенного порога, то не следует применять обоснование.

ОБНОВЛЕНИЕ

У меня есть следующая концепция, которая должна работать, просто нужно получить слово из указанного индекса и вставить правильный пробел:

int lastwordwidth = (Int32)g.MeasureString(" " + lastword, Font).Width;
int extraspace=lines.MaxWidthPixels-(Int32)( g.MeasureString( " "+lineNoLastWord, Font ).Width+lastwordwidth );
int totalspacesneeded = (Int32)Math.Floor((decimal)(extraspace / lines.SpaceWidth));
int spacecount = lineNoLastWord.Count(x => x == ' ');
int currentwordspace = 0;

for ( int i=0; i<spacecount; i++ ) {
    if ( currentwordspace>spacecount ) { currentwordspace = 0; }
    // insert spaces where spaces already exist between each word
    // use currentwordspace to determine which word to replace with a word and another space

    if ( currentwordspace==0 ) {
        // insert space after word
    } else {
        // insert space before word
    }

    currentwordspace++;
}

person Kraang Prime    schedule 07.04.2016    source источник
comment
Да, я видел, что, полагаю, я мог бы выборочно вставлять пробелы, которые были бы быстрее, вызывая меньше графических вызовов.   -  person Kraang Prime    schedule 07.04.2016
comment
Ну, вы можете или лучше вы можете использовать n-пространства, но вам нужно будет довольно точно знать их ширину, что не так просто, как кажется, учитывая, что большинство вызовов измерительных строк не будут такими точными, как можно было бы надеяться. не беспокойтесь о производительности до того, как у вас действительно возникнет проблема. )   -  person TaW    schedule 07.04.2016
comment
Я пытаюсь сделать это, используя расстояние от края минус длина строки. Берем этот результат и делим на пол делитель ширины пространства. Далее будут подсчитываться пробелы, а затем, я не знаю... какая-то процедура вставки пробелов, пока не будет достигнут результат floor(). Он должен быть достаточно точным и распределять пробелы между первыми или последними словами по мере необходимости. У меня есть эта настройка как часть процедуры асинхронной печати в pdf (см. другие мои статьи, я добился значительного прогресса с тех пор, как опубликовал решение)   -  person Kraang Prime    schedule 07.04.2016
comment
@StuartWhitehouse - пользовательский класс, использующий принтер Microsoft pdf. custom newpage позволяет рисовать всю графику на страницах без предварительного вызова печати (об этом говорилось в других несвязанных сообщениях). Прямо сейчас я просто застрял на (Int32)Math.Ceiling((decimal)(extraspace / lines.SpaceWidth)), всегда возвращает только 1, где extraspace равно 49, а lines.SpaceWidth равно 31. Кроме того, у меня не было проблем с рисованием/позиционированием с мерной строкой, так как я позабочусь о том, чтобы dpi/и т. д. соответствовали.   -  person Kraang Prime    schedule 07.04.2016
comment
Исправлена ​​последняя проблема. int totalspacesneeded=(Int32)Math.Ceiling( (decimal)extraspace/(decimal)lines.SpaceWidth ); это то, что должно быть   -  person Kraang Prime    schedule 07.04.2016


Ответы (1)


Я нашел решение для этого, которое работает довольно хорошо. Ниже приведен мой метод DrawString, который поддерживает выравнивание текста и будет ломаться и добавлять новые «Страницы» по мере необходимости. Pages, является объектом List<Image>, а метод NewPage() отвечает за добавление нового изображения в этот список.

/// <summary>
/// Add a new string to the current page
/// </summary>
/// <param name="text">The string to print</param>
/// <param name="align">Optional alignment of the string</param>
public void DrawString(string text, System.Windows.TextAlignment align = System.Windows.TextAlignment.Left, int MaxWidth = -1 ) {
    RectangleF textBounds;
    SolidBrush brush = new SolidBrush( ForeColor );
    StringFormat sf = ConvertAlignment(align);
    LineBreaker lines = breakIntoLines(text, MaxWidth);

    int currentLine = 1;

    int originX = X;

    foreach ( string line in lines.Lines ) {
        // add string to document
        using ( Graphics g=Graphics.FromImage( Pages[CurrentPage - 1] ) ) {
            g.CompositingQuality = CompositingQuality.HighQuality;

            textBounds=new RectangleF( X, Y, lines.MaxWidthPixels, lines.StringHeight );

            if ( align==System.Windows.TextAlignment.Justify ) {

                if ( currentLine<lines.Lines.Count ) {
                    string lastword=line.Split( ' ' ).Last();
                    if ( line.Contains( ' ' ) ) {
                        // routine to caclulate how much padding is needed and apply the extra spaces as evenly as possibly by looping
                        // through the words. it starts at the first word adding a space after if needed and then continues through the
                        // remaining words adding a space before them as needed and excludes the right most word which is printed as right
                        // align always.
                        string lineNoLastWord=line.Substring( 0, line.LastIndexOf( " " ) ).Trim();
                        List<string> words=lineNoLastWord.Split( ' ' ).ToList<string>();
                        int lastwordwidth=(Int32)g.MeasureString( " "+lastword, Font ).Width;
                        int extraspace=lines.MaxWidthPixels-(Int32)( g.MeasureString( " "+lineNoLastWord, Font ).Width+lastwordwidth );
                        int totalspacesneeded=(Int32)Math.Ceiling( (decimal)extraspace/(decimal)lines.SpaceWidth );
                        int spacecount=lineNoLastWord.Count( x => x==' ' );
                        int currentwordspace=0;

                        if ( words.Count>1 ) {
                            while ( totalspacesneeded>0 ) {
                                if ( currentwordspace>spacecount ) { currentwordspace=0; }
                                // insert spaces where spaces already exist between each word
                                // use currentwordspace to determine which word to replace with a word and another space
                                if ( currentwordspace==0 ) {
                                    // insert space after word
                                    words[currentwordspace]+=" ";
                                } else {
                                    // insert space before word
                                    words[currentwordspace]=" "+words[currentwordspace];
                                }
                                currentwordspace++;
                                totalspacesneeded--;
                                if ( totalspacesneeded==0 ) { break; }
                            }
                        }
                        lineNoLastWord=String.Join( " ", words );

                        g.DrawString( lineNoLastWord, Font, brush, textBounds, sf );
                        g.DrawString( lastword, Font, brush, textBounds, ConvertAlignment( System.Windows.TextAlignment.Right ) );
                    } else {
                        // when only 1 word, just draw it
                        g.DrawString( line, Font, brush, textBounds, ConvertAlignment( System.Windows.TextAlignment.Left ) );
                    }
                } else {
                    // just draw the last line
                    g.DrawString( line, Font, brush, textBounds, ConvertAlignment( System.Windows.TextAlignment.Left ) );
                }

            } else {
                g.DrawString( line, Font, brush, textBounds, sf );
            }
        }
        Y+=lines.LineHeight;
        if ( Y+lines.LineHeight>Pages[CurrentPage-1].Height ) {
            NewPage();
            if ( currentLine<lines.Lines.Count ) { X=originX; }
        }
        currentLine++;
    }
}

/// <summary>
/// Break a long string into multiple lines. Is also carriage return aware.
/// </summary>
/// <param name="s">the string</param>
/// <param name="maxLineWidth">the maximum width of the rectangle. if -1, will use the full width of the image</param>
/// <returns></returns>
internal LineBreaker breakIntoLines( string s, int maxLineWidth ) {
    List<string> sResults = new List<string>();

    int stringHeight;
    int lineHeight;
    int maxWidthPixels = maxLineWidth;
    int spaceWidth;

    string[] lines = s.Split(new string[] { "\n", "\r\n" }, StringSplitOptions.None);
    using ( Graphics g=Graphics.FromImage( Pages[CurrentPage - 1] ) ) {
        g.CompositingQuality = CompositingQuality.HighQuality;
        if ( maxLineWidth<=0||maxLineWidth>( Pages[CurrentPage-1].Width-X ) ) {
            maxWidthPixels=Pages[CurrentPage-1].Width-X;
        }
        lineHeight = (Int32)( g.MeasureString( "X", Font ).Height*(float)( (float)LineSpacing/(float)100 ) );
        stringHeight = (Int32)g.MeasureString( "X", Font ).Height;
        spaceWidth=(Int32)g.MeasureString( " ", Font ).Width;
        foreach ( string line in lines ) {
            string[] words=line.Split( new string[] { " " }, StringSplitOptions.None );
            sResults.Add( "" );
            for ( int i=0; i<words.Length; i++ ) {
                if ( sResults[sResults.Count-1].Length==0 ) {
                    sResults[sResults.Count-1]=words[i];
                } else {
                    if ( g.MeasureString( sResults[sResults.Count-1]+" "+words[i], Font ).Width<maxWidthPixels ) {
                        sResults[sResults.Count-1]+=" "+words[i];
                    } else {
                        sResults.Add( words[i] );
                    }
                }
            }
        }
    }
    return new LineBreaker() {
        LineHeight = lineHeight,
        StringHeight = stringHeight,
        MaxWidthPixels = maxWidthPixels,
        Lines = sResults,
        SpaceWidth = spaceWidth
    };
}

/// <summary>
/// Helper method to convert TextAlignment to StringFormat
/// </summary>
/// <param name="align">System.Windows.TextAlignment</param>
/// <returns>System.Drawing.StringFormat</returns>
private StringFormat ConvertAlignment(System.Windows.TextAlignment align) {
    StringFormat s = new StringFormat();
    switch ( align ) {
        case System.Windows.TextAlignment.Left:
        case System.Windows.TextAlignment.Justify:
            s.LineAlignment=StringAlignment.Near;
            break;
        case System.Windows.TextAlignment.Right:
            s.LineAlignment=StringAlignment.Far;
            break;
        case System.Windows.TextAlignment.Center:
            s.LineAlignment=StringAlignment.Center;
            break;
    }
    s.Alignment = s.LineAlignment;
    return s;
}

/// <summary>
/// Class to hold the line data after broken up and measured using breakIntoLines()
/// </summary>
internal class LineBreaker {
    public List<string> Lines { get; set; }
    public int MaxWidthPixels { get; set; }
    public int StringHeight { get; set; }
    public int LineHeight { get; set; }

    public int SpaceWidth { get; set; }

    public LineBreaker() {
        Lines = new List<string>();
        MaxWidthPixels = 0;
        StringHeight = 0;
        LineHeight = 0;
        SpaceWidth = 0;
    }

    public LineBreaker( List<string> lines, int maxWidthPixels, int stringHeight, int lineHeight, int spaceWidth ) {
        Lines = lines;
        MaxWidthPixels = maxWidthPixels;
        LineHeight = lineHeight;
        StringHeight = stringHeight;
        SpaceWidth = spaceWidth;
    }
}

Вышеуказанная комбинация методов поддерживает:

  • Выравнивание по левому краю
  • Правильное выравнивание
  • Выравнивание по центру
  • Выравнивание по ширине — последняя строка, отправленная с выравниванием по ширине, будет напечатана с выравниванием по левому краю, поскольку обычно это конец абзаца.
  • Все отправленные строки будут проверять ограничения, используя изображение или указанную ширину, которая находится в диапазоне между текущей позицией X и краем. Ширина, превышающая или находящаяся в отрицательном диапазоне, будет установлена ​​на расстоянии между X и правой стороной изображения. Каждая линия находится в своей ограничивающей рамке.
  • Линии не обрезаются.
  • Перенос строки разрывается в слове по мере необходимости и при возврате каретки ("\n" или "\r\n")

LineSpacing — это просто целое число, где 100 означает 100% высоты строки. X — целое число для получения/установки позиции X. Y — целое число для получения/установки позиции Y. Font является геттером/сеттером для System.Drawing.Font

person Kraang Prime    schedule 07.04.2016