Justifying a string manually for DrawString() method in c#

时间:2016-04-07 10:53:11

标签: c# string system.drawing justify

I have implemented a fairly rudimentary 'Justify' method for drawing a string, however I would like to optimize it so the spacing is a bit more dispersed.

What I have so far, is this :

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 is a custom method as follows :

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

The result is close, but needs some adjusting of the spaces in the string lineNoLastWord.

A bit more background behind the code. line is the result of a method which is responsible for detecting if the string goes out of bounds (width), and breaking it into lines and words, breaking and measuring as it goes to ensure the full line remains within the width of the area to be drawn on. The method implements other properties in a much larger class, but here is the gist of it :

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

The following image demonstrates the problem that this is causing :

demonstration

I have also seen this stackoverflow question and answers, and found that it is also an inefficient way to space due to the unknown size of the string and the unknown width of the document will result in the string being too long if too many words, or too short with nothing right justified. Full justification means the text lines up on both the left and right side, and typically, the content inside is evenly spaced apart as much as possible. This is how I would like to implement it.

The solution, is likely a calculation on the lastWord and lineNoLastWord strings with some measurements to ensure viability of the output in that no two words in the string will run or bunch together, and there will be no padding on the right side, however left may still contain an indent or tab. Another part to consider, is if the string is shorter than a certain threshold, then no justification should be applied.

UPDATE

I have the following concept, which should work, just need to get the word from the specified index and insert the proper space :

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

1 个答案:

答案 0 :(得分:0)

我找到了一个解决方案,这个方法很有效。以下是我的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;
    }
}

上述方法组合支持:

  • 左对齐
  • 右对齐
  • 中心对齐
  • 对齐对齐 - 使用Justify发送的最后一行,只是左对齐打印,因为这通常是段落的结尾。
  • 发送的所有行将使用图像检查约束,或指定在当前X位置和边缘之间的范围内的宽度。超出或在负范围内的宽度将被设置为X和图像右侧之间的距离。每一行都在它自己的边界框内。
  • 不剪裁线条。
  • 根据需要和回车符(“\ n”或“\ r \ n”)
  • 对换行进行换行

LineSpacing只是一个整数,其中100表示​​100%的LineHeight。 X是用于获取/设置X位置的整数。 Y是用于获取/设置Y位置的整数。 FontSystem.Drawing.Font

的getter / setter