【问题标题】:Justifying a string manually for DrawString() method in c#在 c# 中为 DrawString() 方法手动对齐字符串
【发布时间】:2016-07-28 05:00:12
【问题描述】:

我已经实现了一个相当基本的“对齐”方法来绘制字符串,但是我想对其进行优化,使间距更加分散。

到目前为止,我所拥有的是:

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;
    }
}

下图演示了由此引起的问题:

我也看过this stackoverflow question and answers,发现也是一种低效的空间方式,因为字符串的大小未知,文档的宽度未知,字数过多会导致字符串过长,或者太短,没有任何正确的理由。完全对齐意味着文本在左侧和右侧对齐,并且通常内部的内容尽可能均匀地间隔开。这就是我想要实现的方式。

解决方案,可能是对 lastWordlineNoLastWord 字符串进行计算,并进行一些测量以确保输出的可行性,因为字符串中没有两个单词会运行或聚集在一起,并且右侧不会有填充,但左侧可能仍包含缩进或制表符。要考虑的另一部分是,如果字符串短于某个阈值,则不应应用任何理由。

更新

我有以下概念,应该可以,只需要从指定的索引中获取单词并插入适当的空格:

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++;
}

【问题讨论】:

  • 你没看到this吗?如果计算一个浮点数以知道在 每个 单词之后移动多远,因此它将单词 均匀 分布在整个行中..
  • 是的,我看到我想我可以选择性地插入空格,这样会更快导致更少的图形调用。
  • 好吧,你可以或更好地使用 n 空间,但你需要相当精确地知道它们的宽度,这并不像听起来那么简单,因为大多数测量字符串调用不会像人们希望......但在真正遇到问题之前不要担心性能......(请参阅'premature optimization' ;-)
  • 我正在尝试使用距边缘的距离减去字符串的长度。采用该结果并计算空间宽度的除数。接下来,将计算空间,然后,我不知道......某种插入空间例程,直到满足 floor() 结果。它应该足够精确,并根据需要在第一个或最后几个单词之间展开空格。我将此设置作为异步打印到 pdf 例程的一部分(请参阅我的其他文章,自从我发布解决方案以来取得了相当大的进展)
  • @SanuelJackson,我不清楚你想要实现什么。您想要在最后一个单词之前有一个空格,还是在每个单词之后有一个空格?为什么不直接使用系统理由呢?顺便说一句:您是否知道 MeasureString 对于文本片段的详细操作或插入符号定位往往不准确 - 请参阅remarks in the docs。你提到了PDF输出;最终结果是绘制成 .Net 图形,还是为了将文本注入 PDF 编写器库而进行测量?

标签: c# string system.drawing justify


【解决方案1】:

我想出了一个很好的解决方案。以下是我的 DrawString 方法,它可以识别文本对齐,并且会根据需要中断并添加新的“页面”。 Pages,是一个List&lt;Image&gt; 对象,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 表示 LineHeight 的 100%。 X 是一个整数,用于获取/设置 X 位置。 Y 是一个整数,用于获取/设置 Y 位置。 FontSystem.Drawing.Font 的 getter/setter

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-02
    • 1970-01-01
    • 2016-06-16
    相关资源
    最近更新 更多