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() {}
}

Canvas mit Logo

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

Unterschiedliche Schriftarten auf einem Canvas

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.

Canvas mit Schrift und Bild

Previous Article
Next Article

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.