21.05.02 Texte und Bilder zeichnen
Um Ihr Hello World Canvas
etwas ansprechender zu gestalten, haben Sie die Möglichkeit bspw. Bilder auf Ihrem Canvas
anzuzeigen oder die Schriftart, den Font
, zu verändern. Dies wird Thema in diesem Kapitel sein.
Ein Bild zeichnen
Wenn Sie ein Bild, wie in Kapitel 21.03 High-Level vs. Low-Level GUI beschrieben, auf einem Canvas
darstellen möchten, müssen Sie dieses über die Methode drawImage(Image img, int x, int y, int anchor)
des Graphics
-Objekts der paint
-Methode des Canvas
realisieren. Die Funktion des anchor
haben Sie bereits im letzten Kapitel kennengelernt.
package de.jbb.j2me.taim; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; public class TextAndImageCanvas extends Canvas { private Image img; public TextAndImageCanvas(Image img) { this.img = img; } protected void paint(Graphics g) { g.drawImage(img, getWidth() / 2, getHeight() / 2, Graphics.HCENTER|Graphics.VCENTER); } }
Liegt das zu ladende Bild im Verzeichnis images und trägt den Namen jbb.png, könnte das dazugehörige MIDlet
so aussehen:
package de.jbb.j2me.taim; import java.io.IOException; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Display; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Image; import javax.microedition.midlet.*; public class TextAndImageMIDlet extends MIDlet implements CommandListener { private Display disp; private TextAndImageCanvas canvas; private Command exit; private boolean first = true; public void startApp() { if (first) { first = false; Image canImg = null; try { canImg = Image.createImage("/images/jbb.png"); } catch (IOException ex) { ex.printStackTrace(); } disp = Display.getDisplay(this); canvas = new TextAndImageCanvas(canImg); exit = new Command("Beenden", Command.EXIT, 0); canvas.setTitle("Text and Images Example"); canvas.addCommand(exit); canvas.setCommandListener(this); } disp.setCurrent(canvas); } public void commandAction(Command c, Displayable d) { if (c.equals(exit)) { destroyApp(false); } } public void destroyApp(boolean unconditional) { notifyDestroyed(); } public void pauseApp() {} }
Neben der Methode drawImage
gibt es noch eine andere, etwas komplexere Möglichkeit, ein Bild auf einem Canvas
darzustellen: graphics.drawRegion(Image src, int x_src, int y_src, int width, int height, int transform, int x_dest, int y_dest, int anchor)
.
Mit dieser Methode können Sie einen Ausschnitt des Bildes src
vom Punkt x_src, y_src
jeweils width
Pixel in die Breite und height
Pixel in die Tiefe auswählen. Dieser Ausschnitt wird anhand von transform
um einen bestimmten Winkel gedreht und/oder gespieglet. Hierzu werden die Konstanten TRANS_NONE
(keine Rotation/Spiegelung), TRANS_ROT90, TRANS_ROT180, TRANS_ROT270
(Drehung um die jeweilige Gradzahl), TRANS_MIRROR, TRANS_MIRROR_ROT90, TRANS_MIRROR_ROT180
und TRANS_MIRROR_ROT270
(Spiegelung und ggf. Drehung) der Klasse javax.microedition.lcdui.game.Sprite
verwendet. Der Ausschnitt wird dann an der Stelle x_dest, y_dest
mit einer Ausrichtung am anchor
gezeichnet.
g.drawRegion( img, // Das Bild img 0, // von Punkt X = 0 0, // und Punkt Y = 0 img.getWidth() / 2, // bis zur Hälfte der Breite img.getHeight() / 2, // und der Hälfte der Höhe ausschneiden Sprite.TRANS_ROT180, // um 180 Grad drehen getWidth() / 2, // und getHeight() / 2, // zentriert Graphics.HCENTER|Graphics.VCENTER // zeichnen );
Text zeichnen
Aus dem letzten Kapitel kennen Sie bereits die Methode drawString
von Graphics
um einfachen Text auf einem Canvas
darzustellen. Sie müssen jedoch nicht den kompletten String
zeichnen. Mit der Methode drawSubstring(String str, int offset, int len, int x, int y, int anchor)
des Graphics
-Objekts können Sie von offset
an len
Zeichen des Texts str
an die Position x, y
mit der Ausrichtung anchor
auf die Leinwand bringen.
Liegt Ihr Text in Form von chars
vor, müssen Sie diesen jedoch ebenfalls nicht in einen String
umwandeln. Sie können stattdessen die Methoden drawChar(char c, int x, int y, int anchor)
(zeichnen eines einzelnen Buchstaben) oder drawChars(char[] chars, int offset, int length, int x, int y, int anchor)
verwenden. Dabei ist offset
die Position im char
-Array, ab welcher die Zeichen geschrieben werden sollen (bspw. 0 um ab dem ersten Zeichen zu schreiben) und length
, wie viele Zeichen ab der Position geschrieben werden sollen.
g.drawChar('A', 0, 0, Graphics.LEFT|Graphics.TOP); char[] chars = new char[3]; chars[0] = 'J'; chars[1] = 'B'; chars[2] = 'B'; g.drawChars(chars, 0, chars.length, getWidth(), 0, Graphics.RIGHT|Graphics.TOP);
Eine Schriftart auswählen
Auch wenn dies nicht so umfangreich wie in der Java Desktopumgebung möglich ist – selbst in der Java Micro Edition können Sie die Schriftart verändern. Hierzu setzen Sie einen javax.microedition.lcdui.Font
dem Graphics
-Objekt über setFont(Font f)
bevor Sie den Text schreiben. Neben den Standard-Fonts (Standard-Schrift des Geräts: Font.getDefaultFont()
, Standard des Geräts für statischen Text: Font.getFont(Font.FONT_STATIC_TEXT)
und Standard des Geräts für Texteingaben: Font.getFont(Font.FONT_INPUT_TEXT)
) können Sie auch einen Font
über die Methode Font.getFont(int face, int style, int size)
selbst zusammenstellen. Das face
definiert hierbei die eigentliche Schriftart (proportional: Font.FACE_PROPORTIONAL
, mit identischer Buchstabengröße: Font.FACE_MONOSPACED
oder die Systemschriftart: Font.FACE_SYSTEM
). Über den style
kann der Font gewöhnlich (Font.STYLE_PLAIN
), fett (Font.STYLE_BOLD
), kursiv (Font.STYLE_ITALIC
), unterstrichen (Font.STYLE_UNDERLINED
) oder aus einer Kombination aus mehreren Styles (via oder (|
) verknüpft) dargestellt werden. Der letzte Parameter size
gibt die Größe der Schrift an (klein: Font.SIZE_SMALL
, normal: Font.SIZE_MEDIUM
oder groß: Font.SIZE_LARGE
).
Beachten Sie, dass jeder Font
auf jedem Gerät unterschiedlich aussieht. Möchten Sie an die spezifische Breite oder Höhe eines Textes kommen, können Sie die Höhe allgemein über font.getHeight()
bzw. die Breite textspezifisch über font.stringWidth("My String")
abfragen. Ein kleines Beispiel in unserem TextAndImageCanvas
:
package de.jbb.j2me.taim; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Font; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; public class TextAndImageCanvas extends Canvas { private Image img; private Font system; private Font def; private Font input; private Font font1; private Font font2; public TextAndImageCanvas(Image img) { this.img = img; system = Font.getFont(Font.FONT_STATIC_TEXT); def = Font.getDefaultFont(); input = Font.getFont(Font.FONT_INPUT_TEXT); font1 = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD|Font.STYLE_ITALIC, Font.SIZE_LARGE); font2 = Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_UNDERLINED, Font.SIZE_SMALL); } protected void paint(Graphics g) { int yPos = 0; // y-Koordinate, an der der Text geschrieben wird g.drawImage(img, getWidth() / 2, getHeight() / 2, Graphics.HCENTER|Graphics.VCENTER); g.setColor(255, 255, 255); g.setFont(def); g.drawString("Default Font", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos += g.getFont().getHeight() + 2; g.setFont(system); g.drawString("System Font", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos += g.getFont().getHeight() + 2; g.setFont(input); g.drawString("Input Font", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos += g.getFont().getHeight() + 2; g.setFont(font1); g.drawString("font1", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos += g.getFont().getHeight() + 2; g.setFont(font2); g.drawString("font2", 5, yPos, Graphics.TOP|Graphics.LEFT); } }
Text am Zeilenende umbrechen
Aufgrund der unterschiedlichen Displaygrößen und Font
-Darstellungen der Handys kann es sein, dass bei Handy A der Text mit Font
B problemlos in eine Zeile passt, bei Handy C der selbe Text mit dem selben Font
jedoch abgeschnitten wird. Um dies zu umgehen, benötigen Sie eine Funktion, die einen String
in X Teile zerlegt, wobei jedes Teil auf dem jeweiligen Endgerät in genau eine Zeile passt.
Hierzu wird eine Methode definiert, die ein String
-Array (jedes Element im Array entspricht einer Zeile) anhand eines gegeben Ursprungtextes, eines spezifischen Fonts
und einer maximalen Breite zurückliefert (Kommentare zur Funktionsweise finden Sie im Quellcode):
package de.jbb.j2me.taim; import javax.microedition.lcdui.Font; public class TextTools { public static String[] getViewableTextLines(String text, Font f, int width) { // String-Array mit der geschätzten Zeilenanzahl anlegen. // Diese Größe entspricht der Gesamtbreite des Textes, geteilt durch die // verfügbare Breite. Da nur an Leerzeichen umgebrochen wird, ist es // unrealistisch, dass jede Zeile die Breite komplett ausfüllt. Deshalb // wird von der maximalen Breite noch etwas abgezogen. String[] ret = new String[f.stringWidth(text) / (width - f.stringWidth("012345")) + 1]; // Wurde errechnet, dass der Text in eine Zeile passt, kann dieser // direkt in ein Array umgewandelt und zurückgegeben werden. if (ret.length == 1) { return new String[]{text}; } // Variable, die die aktuelle Position (Zeile) speichert int arPos = 0; // Schleife, die den Text durchläuft. Abbruchbedingungen befinden sich // direkt in der Schleife. curPos ist das aktuelle Leerzeichen im Text, // lastCut die Stelle, an der der letzte Zeilenumbruch stattgefunden // hat, lastAcceptPos ist die Position, an der das letzte Leerzeichen // ohne nötigen Zeilenumbruch festgestellt wurde, lastWord ist die // Stelle, an der das letzte Leerzeichen ermittelt wurde. for (int curPos = 0, lastCut = 0, lastAcceptPos = 0, lastWord = 0;;) { // Werden mehr Zeilen benötigt, als zuerst geschätzt, wird die // Anzahl der Zeilen im Array um den Faktor 1 erhöht if (arPos >= ret.length) { String[] tmp = new String[ret.length + 1]; System.arraycopy(ret, 0, tmp, 0, ret.length); ret = tmp; } // Letztes Leerzeichen aktualisieren lastWord = curPos; // Aktuelles Leerzeichen aktualisieren curPos = text.indexOf(' ', curPos + 1); // Wenn keine weiteren Leerzeichen gefunden wurden if (curPos < 0) { // Wenn der restliche String in eine Zeile passt, oder der // letzte Zeilenumbruch mit der zuletzt akzeptierten Position // übereinstimmt (Letztes Wort ist zu groß für eine Zeile) if (f.substringWidth(text, lastCut, text.length() - lastCut) < width || lastCut == lastAcceptPos) { // Schreibe diesen an die aktuelle Position ret[arPos] = text.substring(lastCut); // und beende die Schleife break; } // Ansonsten wird eine neue Zeile mit dem Text zwischen dem // letzten Zeilenumbruch und der zuletzt akzeptierten Position // eingefügt. Die Schleife läuft weiter. else { ret[arPos++] = text.substring(lastCut, lastAcceptPos); // Variablen aktualisieren lastCut = lastAcceptPos; curPos = lastCut; } } // Falls weitere Leerzeichen gefunden wurden else { // Falls der Text zwischen dem letzten Zeilenumbruch und dem // aktuellen Leerzeichen in eine Zeile passt, überprüfe beim // nächsten Durchlauf, ob dies für das nächste Wort auch noch // gültig ist. if (f.substringWidth(text, lastCut, curPos - lastCut) < width) { lastAcceptPos = curPos + 1; } // Falls nicht, und falls die zuletzt akzeptierte Position nicht // mit dem letzten Zeilenumbruch übereinstimmt, entspricht // der Text zwischen dem letzten Zeilenumbruch und dem zuletzt // akzeptierten Leerzeichen der aktuellen Zeile. else if (lastAcceptPos != lastCut) { ret[arPos++] = text.substring(lastCut, lastAcceptPos); lastCut = lastAcceptPos; } // Ansonsten entspricht der Text zwischen dem letzten // Zeilenumbruch und dem letzten Wort der aktuellen Zeile else { ret[arPos++] = text.substring(lastCut, lastWord); // Variablen aktualisieren curPos = lastWord; lastAcceptPos = curPos + 1; lastCut = lastAcceptPos; } } } // Wurden zu viele Zeilen geschätzt, wird die Größe des Arrays // auf die tatsächliche Zeilenzahl reduziert. if (arPos + 1 != ret.length) { String[] tmp = new String[arPos + 1]; System.arraycopy(ret, 0, tmp, 0, tmp.length); ret = tmp; } return ret; } }
Schriftgröße an Text anpassen
Manchmal ist jedoch auch der umgekehrte Fall gegeben. Sie haben einen Text, der zwingend in eine Zeile geschrieben werden muss. Dieser soll aber so groß wie möglich dargestellt werden. Für diesen Zweck können Sie sich eine Methode schreiben, die einen größtmöglichen Font
initialisiert (STYLE_BOLD
und SIZE_LARGE
), und dann in einer Schleife prüft, ob der gewünschte Text mit diesem Font
in den gegebenen Platz passt. Ist dies nicht der Fall, wird der Font
schrittweise kompakter gemacht (Von groß und fett auf groß, von groß auf normal und fett, von normal und fett auf normal, …).
public static Font getBiggestFont(String line, int width) { Font f = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD, Font.SIZE_LARGE); while (f.stringWidth(line) > width) { if (f.isBold()) { f = Font.getFont(f.getFace(), Font.STYLE_PLAIN, f.getSize()); } else { int newSize = f.getSize(); switch (f.getSize()) { case Font.SIZE_LARGE: newSize = Font.SIZE_MEDIUM; break; case Font.SIZE_MEDIUM: newSize = Font.SIZE_SMALL; break; default: return f; } f = Font.getFont(f.getFace(), Font.STYLE_BOLD, newSize); } } return f; }
Selbstverständlich kann diese Methode aber auch nichts daran ändern, wenn der Text selbst mit dem kleinstmöglichen Font
nicht in eine Zeile passt. Je nach Anforderung kann es auch nützlich sein, diese Methode für ein ganzes String
-Array umzuschreiben. Falls mehrere solche Texte in der Anwendung angezeigt werden sollen, kann so die größtmögliche Schriftart ermittelt werden.
Zusammenfassung Canvas
Um die neuen Funktionen zu testen, kann das TextAndImageCanvas
entsprechend ergänzt werden:
package de.jbb.j2me.taim; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Font; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; public class TextAndImageCanvas extends Canvas { private Image img; private Font system; private Font def; private Font input; private Font font1; private Font font2; private Font biggest; private String[] longStr; private String oneLine = "Steht immer in einer einzigen Zeile!"; public TextAndImageCanvas(Image img) { this.img = img; system = Font.getFont(Font.FONT_STATIC_TEXT); def = Font.getDefaultFont(); input = Font.getFont(Font.FONT_INPUT_TEXT); font1 = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD|Font.STYLE_ITALIC, Font.SIZE_LARGE); font2 = Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_UNDERLINED, Font.SIZE_SMALL); biggest = TextTools.getBiggestFont(oneLine, getWidth()); longStr = TextTools.getViewableTextLines( "Hier steht sehr viel Text, der eigentlich am Ende des Canvas umgebrochen werden muss, " + "und es auch (dank der TextTools) wird!" , font2, getWidth() - 10); } protected void paint(Graphics g) { int yPos = 0; // y-Koordinate, an der der aktuelle Font geschrieben wird g.drawImage(img, getWidth() / 2, getHeight() / 2, Graphics.HCENTER|Graphics.VCENTER); g.setColor(255, 255, 255); g.setFont(def); g.drawString("Default Font", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos += g.getFont().getHeight() + 2; g.setFont(system); g.drawString("System Font", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos += g.getFont().getHeight() + 2; g.setFont(input); g.drawString("Input Font", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos += g.getFont().getHeight() + 2; g.setFont(font1); g.drawString("font1", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos += g.getFont().getHeight() + 2; g.setFont(font2); g.drawString("font2", 5, yPos, Graphics.TOP|Graphics.LEFT); yPos = getHeight(); for (int i = longStr.length - 1; i > -1; i--) { g.drawString(longStr[i], 5, yPos, Graphics.LEFT|Graphics.BOTTOM); yPos -= g.getFont().getHeight() + 2; } g.setFont(biggest); g.drawString(oneLine, getWidth() / 2, yPos - g.getFont().getHeight(), Graphics.HCENTER|Graphics.BOTTOM); } }
Die Änderungen finden Sie in den Zeilen 16-18, 27-31 und 59-66.