czwartek, 17 kwietnia 2008

Pola tekstowe o wysokości zależnej od rodzaju rekordu

Ostatnio przygotowując raport w technologii RDLC stanąłem przed koniecznością ustalenia wysokości pól tekstowych w zależności od typu oferty, tak by rekordów typu A było tylko cztery na stronie, typu B sześć, a C 10.

Z tej racji, że raporty rdlc mogą być renderowane w różny sposób, to nie generują zdarzeń pozwalający na bierząco korygować wysokość (albo też generować indeks czy spis treści). Stąd też wybrałem inne rozwiązanie: ustalenie właściwości "CanGrow" pola tekstowego na true oraz zmienianie liczby linii zawartych w odpowiedniej komórce tabeli źródłowej. Teraz pozostało opracować kod do wyliczania wysokości, aby się łatwiej testował, to najpierw zdefiniowałem pomocniczy interfejs:

public interface ITextMeasurer
{
int NoOfLines(string text);
}



A potem jego implementacja:



using System;
using System.Drawing;

namespace Utilities.Graphics
{
public class TextMeasurer : ITextMeasurer, IDisposable
{
private readonly SizeF layoutArea;
private readonly Font font;
private readonly System.Drawing.Graphics graphics;
private readonly float oneLineHeight;

public TextMeasurer(string fontName, float fontSize,
float maxWidthInCm, float maxHeightInCm,
byte totalHorizontalPadding) :

this(new Font(fontName, fontSize), maxWidthInCm, maxHeightInCm,
totalHorizontalPadding)
{}

public TextMeasurer(Font font, float maxWidthInCm,
float maxHeightInCm,
byte totalHorizontalPadding)
{
this.font = font;
Bitmap bmp = new Bitmap(1, 1);
graphics = System.Drawing.Graphics.FromImage(bmp);
float width = (float)(maxWidthInCm / 2.54) *
bmp.HorizontalResolution - totalHorizontalPadding;

float height = maxHeightInCm * bmp.VerticalResolution;
layoutArea = new SizeF(width, height);

oneLineHeight = GetTextSize("W").Height;
}

public SizeF GetTextSize(string text)
{
return graphics.MeasureString(text, font, layoutArea,
StringFormat.GenericTypographic);
}

public int NoOfLines(string text)
{
float textHeight = GetTextSize(text).Height;
return (int)(textHeight / oneLineHeight);
}

#region IDisposable Members

public void Dispose()
{
graphics.Dispose();
}

#endregion
}
}



Jak widać, w powyższym rozwiązaniu korzystam z metody MeasureString pomocniczego obiektu graphics. Tak więc pozostało wykorzystać wyliczenie wysokości tekstu i w razie potrzeby dodać pare linii:



using System.Text;
using Sgh.Utilities.Graphics;

namespace Utilities.Texts
{
public class Corrector
{
private readonly ITextMeasurer textMeasurer;

public Corrector(ITextMeasurer textMeasurer)
{
this.textMeasurer = textMeasurer;
}

public string CorrectHeight(string inputString, int outputLinesCount)
{
int currentNoOfLines = textMeasurer.NoOfLines(inputString);

if (currentNoOfLines == outputLinesCount)
{
return inputString;
}

return AddEmptyLinesToString(inputString,
outputLinesCount - currentNoOfLines);
}

private static string AddEmptyLinesToString(
string inputString, int noLinesToAdd)
{
StringBuilder builder = new StringBuilder();
builder.Append(inputString);

for (int i = 0; i < noLinesToAdd; i++)
{
builder.Append("\r\n ");
}

return builder.ToString();
}
}
}



Jednak okazało się, że czasami w danych źródłowych jest więcej linii tekstu niż może pomieścić w komórcę, tak więc oprócz dodawania pustych wierszy konieczne jest dodanie przycinanie nadmiarowych, tak więc zmodyfikowałem kod metody CurrentHeight:



public string CorrectHeight(string inputString, int outputLinesCount)
{
int currentNoOfLines = textMeasurer.NoOfLines(inputString);

if (currentNoOfLines > outputLinesCount)
{
LineRemover remover = new LineRemover(inputString,
outputLinesCount, textMeasurer);

inputString = remover.OutputString;
currentNoOfLines = remover.TextHeight;
}

if (currentNoOfLines == outputLinesCount)
{
return inputString;
}

return AddEmptyLinesToString(inputString,
outputLinesCount - currentNoOfLines);
}



Jak widać teraz wykorzystywany jest obiekt klasy LineRemover. Tnąc tekst wzdłuż spacji  / znaków końca wiersza (za pomocą wyrażenia regularnego), wykorzystuje wyszukiwanie binarne do znaleznienia takiego miejsca, dla którego długość ciągu jest największa, a wysokość napisu jest nie wyższa od wymaganej.



using System.Collections.Generic;
using System.Text.RegularExpressions;
using Utilities.Graphics;

namespace Utilities.Texts
{
public class LineRemover
{
private readonly string outputString;
private int textHeight;

public LineRemover(string inputString, int outputLinesCount,
ITextMeasurer textMeasurer)
{
this.outputString = RemoveLines(inputString,
outputLinesCount, textMeasurer);
}

public string OutputString
{
get { return this.outputString; }
}

public int TextHeight
{
get { return this.textHeight; }
}

private string RemoveLines(string inputString,
int outputLinesCount,
ITextMeasurer textMeasurer)
{
List<int> whitespaces = FindWhitespaces(inputString);

int lowIndex = 0;
int highIndex = whitespaces.Count;

while (highIndex > lowIndex + 1)
{
int currentIndex = (lowIndex + highIndex) / 2;
string currentText = inputString.Substring(0,
whitespaces[currentIndex]);

textHeight = textMeasurer.NoOfLines(currentText);

if (textHeight > outputLinesCount)
{
highIndex = currentIndex;
}
else
{
lowIndex = currentIndex;
}
}

return inputString.Substring(0, whitespaces[lowIndex]);
}

private List<int> FindWhitespaces(string inputString)
{
string pattern = @"\s+";
Regex regex = new Regex(pattern, RegexOptions.Compiled);

MatchCollection matches = regex.Matches(inputString);
List<int> whitespaces = new List<int>();
whitespaces.Add(0); // first char
foreach (Match match in matches)
{
whitespaces.Add(match.Index);
}

whitespaces.Add(inputString.Length);

return whitespaces;
}
}
}



Przykład wykorzystania:



float widthInCm = 8f;

using (TextMeasurer textMeasurer = new TextMeasurer(
"Arial", 8f, widthInCm, 20f, 4))
{
Corrector textCorrector = new Corrector(textMeasurer);

foreach(FramesDataSet.OffersListRow oneOffer in offers)
{
string authorsString = oneOffer.AuthorsString;
int linesCount = textMeasurer.NoOfLines(oneOffer.AuthorsString);

if (oneOffer.OfferType == 3)
{
authorsString = textCorrector.CorrectHeight(authorsString, 2);
}
else if (oneOffer.Level == 1 || oneOffer.Level == 2)
{
authorsString = textCorrector.CorrectHeight(authorsString, 27);
}
else
{
authorsString = textCorrector.CorrectHeight(authorsString, 13);
}
oneOffer.AuthorsString = authorsString;
}
}



Raport prawie gotowy, pozostał tylko problem z niedziałającym zgodnie z oczekiwaniami PageBreak (zamiast nowej strony jest nowa kolumna). Ale to temat na kolejny artykuł ;).

0 komentarze: