Algorithmus für automatische Zeilenumbrüche

Status
Nicht offen für weitere Antworten.

Mewel

Aktives Mitglied
Hallo,

ich bin auf der Suche nach einem Algorithmus der mir einen Text automatisch mit
Zeilenumbrüchen formatiert. Wichtig ist dabei das man verschiedene Sachen
"einstellen" kann:
- maximale Anzahl der Pixel pro Zeile (eine Zeile darf z.B. nur max. 500 pixel breit sein, danach automatischer Zeilenumbruch)
- Bestimmung eines oder mehrerer Trennern (z.B. leerzeichen, komma, punkt etc.)
- maximale Anzahl der Zeilen -> evt. verkleinerung der font-size & wenn text immer noch zu lang dann kürzen und mit "..." abschließen

Wenn ihr Algorithmen kennt die davon Teilbereiche lösen, würde mir das auch schon helfen.

Ich habe mich auch selber mal daran versucht, bin aber nicht 100% mit dem Ergebnis zufrieden. Wenn ihr dort Verbesserungen seht, könnt ihr mir die gerne mitteilen 🙂.

Code:
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.font.TextAttribute;
import java.text.AttributedString;

import sun.font.FontDesignMetrics;

public class MultiLineTextDrawer {

	public final static int AL_LEFT = 0;
	public final static int AL_CENTER = 1;
	public final static int AL_RIGHT = 2;
	
	private Graphics2D graphics;

	private int startX = 0;
	private int startY = 0;
	private int maxLineWidth = 800;
	private int maxSubLines = 50;
	private int startFontSize = 12;
	private int minFontSize = 8;
	private int inset = 4;

	int alignment = AL_LEFT;
	
	public MultiLineTextDrawer(Graphics2D graphics) {
		this.graphics = graphics;
	}

	public void drawText(String text) {
		if(graphics == null)
			return;

		int aktFontSize = startFontSize;
		int aktSubLine = 0;

		String[] subLines = new String[1];
		subLines[0] = text;

		Font font = new Font("Arial", Font.PLAIN, aktFontSize);
		// Erstellung von mehrzeiligem Text
		int textWidth = FontDesignMetrics.getMetrics(font).stringWidth(text);
		if( textWidth > maxLineWidth ) {
			int subLinesNeeded = Math.round((((float)textWidth / (float)maxLineWidth) + 0.5f));
			
			if(subLinesNeeded  > maxSubLines)
				subLinesNeeded = maxSubLines;

			// Teile den text in möglichst 'subLinesNeeded' Teile
			String[] subStrings = text.split(" ");

			// Berechne die Einzelnen Gewichte für jeden subString
			float[] subStringWeights = new float[subStrings.length];
			for(int i = 0; i < subStrings.length; i++) {
				if(i != subStrings.length - 1)
					subStrings[i] += " ";
				subStringWeights[i] = ((float)subStrings[i].length() / (float)text.length());
			}

			// Können nur soviele Zeilen existieren wie auch Leerzeichen
			if(subStrings.length < subLinesNeeded)
				subLinesNeeded = subStrings.length;

			// subLines mit "" inialisieren (sonst wird "null" an String gehängt)
			subLines = new String[subLinesNeeded];
			for( int i = 0; i < subLines.length; i++) {
				subLines[i] = "";
			}

			// subLines werden über die Gewichte mit den subStrings gefüllt
			float aktWeight = 0.0f;
			for(int i = 0; i < subStrings.length; i++) {
				subLines[aktSubLine] += subStrings[i];
				aktWeight += subStringWeights[i];
				if( i == subStrings.length - 1 )
					break;
				if( (aktWeight + subStringWeights[i+1]) > (1.0f / subLinesNeeded) ) {
					if( aktSubLine < subLines.length - 1 ) {
						aktWeight = 0.0f;
						aktSubLine++;
					}
				}
			}
		}

		// Bei immer noch zu große Zeilen muss die Font Größe editiert werden
		boolean correctSize = false;
		while(!correctSize) {
			correctSize = true;
			for( int i = 0; i < subLines.length; i++ ) {
				font = font.deriveFont((float)aktFontSize);
				int subLineWidth = FontDesignMetrics.getMetrics(font).stringWidth(subLines[i]);
				if( subLineWidth > maxLineWidth ) {
					aktFontSize +=- 1;
					correctSize = false;
					break;
				}
			}
		}

		// Zeichne Strings
		graphics.setColor(Color.BLACK);

		for( int subCol = 0; subCol < subLines.length; subCol++) {
			// hack: sollte schon vorher bereinigt werden -> keine 0 length Strings
			if(subLines[subCol].length() == 0)
				continue;
			AttributedString drawString = new AttributedString(subLines[subCol]);
			drawString.addAttribute(TextAttribute.FONT, font);
			int y = startY - ((subLines.length - (subCol + 1)) *  (aktFontSize + inset));
			int x = startX;

			textWidth = FontDesignMetrics.getMetrics(font).stringWidth(subLines[subCol]);
			int descent = FontDesignMetrics.getMetrics(font).getDescent();
			
			if( alignment == AL_CENTER )
				x = ((startX + maxLineWidth) / 2) - (textWidth / 2);
			if( alignment == AL_RIGHT )
				x = maxLineWidth - textWidth;

			graphics.setClip(x, y - aktFontSize, textWidth, (aktFontSize + descent));
			graphics.drawString(drawString.getIterator(), x, y);
		}
	}

	public int getInset() {
		return inset;
	}

	public void setInset(int inset) {
		this.inset = inset;
	}

	public int getMaxLineWidth() {
		return maxLineWidth;
	}

	public void setMaxLineWidth(int maxLineWidth) {
		this.maxLineWidth = maxLineWidth;
	}

	public int getMaxSubLines() {
		return maxSubLines;
	}

	public void setMaxSubLines(int maxSubLines) {
		this.maxSubLines = maxSubLines;
	}

	public int getStartFontSize() {
		return startFontSize;
	}

	public void setStartFontSize(int startFontSize) {
		this.startFontSize = startFontSize;
	}

	public int getStartX() {
		return startX;
	}

	public void setStartX(int startX) {
		this.startX = startX;
	}

	public int getStartY() {
		return startY;
	}

	public void setStartY(int startY) {
		this.startY = startY;
	}
	
	public Graphics2D getGraphics() {
		return graphics;
	}

	public void setGraphics(Graphics2D graphics) {
		this.graphics = graphics;
	}

	public int getAlignment() {
		return alignment;
	}

	public void setAlignment(int alignment) {
		this.alignment = alignment;
	}
}
 
nur ein kurzer Kommentar beim überfliegen den Codes:

Code:
// Können nur soviele Zeilen existieren wie auch Leerzeichen 
         if(subStrings.length < subLinesNeeded) 
            subLinesNeeded = subStrings.length;
"Ein Leerzeichen"
->
"Ein
Leerzeichen"

Wie man sieht:
Code:
MaxZeilen = Leerzeichen+1
 
Erstmal zum letzgenannten Punkt: Das wird hakelig: Sobald man die Fontgröße ändert, ändern sich auch alle Umbrüche - d.h. wenn man merkt, dass die höhe nicht passt, muss man den Font kleiner machen und es nochmal versuchen.

Die ersten beiden sind ... ja, üblich. Da war auch schonmal ein Thread dazu ... http://www.java-forum.org/de/viewtopic.php?p=410253 (und bei der Suche eben habe ich festgestellt, dass ich dem Threadersteller damals nicht mehr geantwortet hatte 😳 ). Man kann sowas (wie dort auch schon steht) als "Kürzeste-Wege-Problem" beschreiben.
 
@SebiB90
subStrings ist dann aber ein Array mit zwei Elementen:
"Ein"
"Leerzeichen"
subStrings.length is dann auch zwei.

Oder ich versteh deine Antwort grad nicht ^^?

@Marco13
Ja das mit der Fontgröße stimmt schon. Ist mir bisher noch nichts besseres eingefallen. Wahrscheinlich müsste ich das in einer Schleife laufen lassen. Das immer wenn die Fontgröße verkleinert wird, nochmal alle subStrings neu berechnet werden. Danach wieder mit der Zeilengröße vergleichen und gegebenfalls die Fontgröße ändern usw.

Den Thread und das "Kürzeste-Wege-Problem" werde ich mir mal genau anschauen.
 
Du darfst keine Klassen aus Sun Packages verwenden!
Schau dir TextLayout und alles was dazu gehört an, damit lässt sich das umsetzen.
 
Hm. Kommt vielleicht drauf an, was man erreichen will. TextLayout und der "normale" LineBreakMeasurer verwenden (laut Doku) einen ziemlich einfachen Algorithmus, der mit vermutlich keinen "schönen" Ergebnisse liefert. Aber vielleicht reichen sie ja...
 
Ok, werd ich mir mal anschauen. Aber warum darf ich keine Klassen aus dem Sun Package verwenden? Gibts dafür nen speziellen Grund?
 
Die sind nicht Teil der offiziellen API. Jemand anderes könnte sich "sein eigenes Java" schreiben, das GENAU SO funktioniert, wie das von Sun - aber diese Klassen zufällig nicht enthält.
 
Hab das jetzt mal getestet und funktioniert auch auf den ersten Blick recht gut. Leider gibt es ein Problem mit dem LineBreakMeasurer. Er gibt mir leider keine Infos darüber, ob der Zeilenumbruch in einem Wort passiert ist (Methode: nextOffset). In diesem Fall wollte ich erstmal die Fontsize verkleinern und danach nochmal den Zeilenumbruch testen.

Nun ist in der Klasse auch noch alles private und sie selbst final. Die Klasse zu kopieren und mit nem neuen Namen zu versehen erscheint mir ein wenig unsauber, zumal sie noch Abhängigkeiten zu CharArrayIterator und TextMeasurer hat, die auch private sind.

Jemand ne Idee?
 
Eigentlich müßte da jetzt ja Wildcard was dzu sagen :meld: :wink:
Ich hab auch noch NICHTS mit dem LineBreakMeasurer und dem TextLayout gemacht, aber beim ersten Block auf die API fällt die Methode
nextLayout(float wrappingWidth, int offsetLimit, boolean requireNextWord)
auf - müßte man damit nicht indirekt rauskriegen können, ob das, was man machen will, noch in die aktuelle Zeile passt?!
 
Ja, das sieht gar nich schlecht aus. Hab ich doch glatt übersehen. Werd das dann gleich mal testen. Danke.
 
Status
Nicht offen für weitere Antworten.

Neue Themen


Zurück
Oben