Externe Schriften:
Im ersten Teil wurde nur der interne Font mit 5x7 Punkten verwendet. Bei hochauflösenden Displays ist die Schrift dann zu klein. Es gibt aber auch die Möglichkeit, einen externen Font zu verwenden. Im Ordner Fonts der Adafruit GFX-Library gibt es eine Anzahl von verschiedenen Fonts, die allerdings nur die ersten 127 Zeichen unterstützen und daher keine deutschen Umlaute haben. Mit dem Programm GFX-Tool können Sie diese Fonts erweitern, oder mit einem Knopfdruck einen neuen Font aus einem Font, der auf Ihrem Rechner installiert ist, erzeugen und im erforderlichen Format als Include-Datei abspeichern. Das Programm steht als Installationspaket für Windows 32-Bit oder 64-Bit zum kostenfreien Download zur Verfügung. Eine Bedienungsanleitung gibt es auf der zugehörigen Wiki Seite. Dieses Programm wird auch noch später in diesem Beitrag zum Anpassen von Bildern herangezogen.
Die Font Include-Datei besteht aus zwei Arrays und einer Struktur, die alle nötigen Informationen zusammenfasst. Die Daten werden im Programmspeicher abgelegt. Ein Array enthält die Pixelmuster für die einzelnen Zeichen. Das andere Array enthält für jeden Buchstabe seine Abmessungen und einen Zeiger auf das Pixelmuster für den Buchstaben. Die Struktur hat üblicherweise denselben Namen wie die Include-Datei.
Funktionen
Neben den aus dem Teil 1 bekannten Funktionen gibt es noch weitere Funktionen, die die Textausgabe betreffen.
Mit der Funktion
void setFont(const GFXfont *f = NULL);
wird ein externer Font gesetzt. Ist der Parameter f = NULL wird der interne Font benutzt.
Da externe Schriften oft auch unterschiedliche Breiten für ihre Zeichen haben, ist es zur Formatierung wichtig zu wissen, wie viele Pixel ein bestimmter Text hoch und breit ist. Die Funktion
void getTextBounds(const char *string, int16_t x, int16_t y, int16_t *x1, int16_t *y1, uint16_t *w, uint16_t *h);
für C-Strings oder
void getTextBounds(const String &str, int16_t x, int16_t y, int16_t *x1, int16_t *y1, uint16_t *w, uint16_t *h);
für Arduino Strings liefert das Rechteck, das den Text exakt umschließt.
Die Cursor-Position x und y gibt den Startpunkt des Textes auf der Grundlinie (grün) an. Im Gegensatz zum internen Font, bei dem die Cursor-Position dem linken oberen Eck entspricht.
Mit der Funktion
void setTextBound(int16_t x, int16_t y, int16_t w, int16_t h);
kann ein Rechteck definiert werden, in dem Textausgaben möglich sind. Text außerhalb dieses Rechtecks wird nicht dargestellt.
Nützliche Hilfsfunktionen
Der Zeichensatz von Schriften, die mit dem GFX-Tool erzeugt wurden, entspricht der Windows Codepage 1250. Da die Arduino IDE UTF8 als Zeichensatz benutzt, ist eine Konvertierung notwendig, um Sonderzeichen korrekt darzustellen.
String utf8ToWindows(String s) {
String res = ""; //String for result
uint8_t prev = 0; //to remember previous character
uint8_t c; //current character
for (uint16_t i = 0; i<s.length(); i++) { //we iterate over the source string
c = byte(s[i]); //get next character
if (c < 0x80) {
res += char(c); //less then 0x80 -> standard ASCII
} else {
switch (prev) {
case 0xC2 : res += char(c); break;
// if previous character code was 0xc2, we can use character as is
case 0xC3 : res += char(c | 0x40); break;
// if prvious character code was 0xc3, we need to set bit 7
case 0x82 : if (c == 0xac) res += char(0x80); break;
//special handling for Euro character
} //other UTF8 characters will be ignored
}
prev = c; //remember currend character for next loop
}
return res;
}
Zeichen kleiner als 128 können direkt übernommen werden. Andere Zeichen werden in UTF8 mit zwei Zeichen codiert. Das erste Zeichen definiert eine Gruppe von 64 Zeichen mit dem Code 0x80 – 0xBF. Die Gruppen 0xC2 und 0xC3 stellen Zeichen dar, die auch in der Codepage 1250 definiert sind. Die erste Gruppe 0xC2 kann direkt übernommen werden 0x80 bis 0xBF. Bei der zweiten Gruppe 0xC3 müssen die beiden höchstwertigen Bits gesetzt sein, also 0xC0 bis 0xFF statt 0x80 bis 0xBF. Ein Sonderfall ist das Euro Zeichen, das in UTF8 mit drei Zeichen codiert wird, 0xE2, 0x82 und 0xAC. In der Codepage 1250 wird das Euro Zeichen mit 0x80 codiert.
Eine weitere praktische Funktion kann dazu verwendet werden, einen Text innerhalb einer Box zu platzieren. Dabei kann eine Ausrichtung horizontal und vertikal erfolgen.
void textInTheBox(String txt, int16_t x, int16_t y, uint16_t width, uint16_t height, uint8_t hor, uint8_t vert) {
int16_t x1,y1,ul, ol, xc, yc, bl;
uint16_t w,h;
tft->getTextBounds(txt,0,0,&x1,&y1,&w,&h);
//we get the text dimensions if printing at x = 0 and y = 0
ol = y1 * -1; //the ascender equates direct to y1 * -1
ul = h-ol; // the descender equates to the hight - ascender
tft->setTextBound(x,y,width,height);
//this call sets clipping to the size of the box
tft->setTextWrap(false); //we dont want text wrapping
xc = x; //for leFt aligned startposition = x
switch (hor) {
case 1 : xc = (x + width/2 - w/2); break;
//for centered startposition is half text width before box center
case 2 : xc = (x + width - w); break;
//for right startposition is text width before box end
}
yc = y + ol;
//for top align startposition is ascender height below top of the box
switch (vert) {
case 1 : yc = y + height/2 + ol/2; break;
//for middle startpos is half ascender height below middle of the box
case 2 : yc = y +height -ul; break;
//for bottom startpos is descender height hiher then bottom of the box
}
tft->setCursor(xc, yc); //set cursor on startposition
tft->print(txt); //print the text
}
In dieser Funktion werden die zwei weiter oben vorgestellten Funktionen setTextBound und getTextBounds genutzt, um den Text zu platzieren. Als Parameter werden, der Text und die Position und Größe der Box benötigt. Es kann die horizontale Ausrichtung links = 0, zentriert = 1 oder rechts = 2 und eine vertikale Ausrichtung oben = 0, mittig = 1 oder unten = gewählt werden.
Zuerst wird die Breite und Höhe des Textes ermittelt. Die Oberlängen erhält man direkt aus der Y-Position des Rechtecks, da der Text auf der Position 0 angenommen wird. Zum Zentrieren wird der Startpunkt auf die halbe Textlänge vor der Mitte der Box gesetzt. Dasselbe erfolgt vertikal, wobei aus optischen Gründen Unterlängen nicht berücksichtigt werden.
Bilder:
Bilder werden als eine Folge von Bildpunkten definiert. Es gibt drei Formate 1-Bit pro Bildpunkt, 8-Bit pro Bildpunkt und 16-Bit pro Bildpunkt. Da Bilder relativ viel Speicherplatz benötigen, werden sie vorzugsweise im Programmspeicher gespeichert.
1-Bit pro Bildpunkt:
Dieses Format benötigt den geringsten Speicherplatz. Bilder sind zweifarbig. Je nachdem, ob das Bit für einen Bildpunkt gesetzt ist oder nicht, wird die eine oder die andere Farbe verwendet. Die Bits werden von links oben nach rechts unten abgearbeitet. Eine neue Bildzeile fängt immer mit einem neuen Byte an. Da die Bildbreite nicht immer ein Vielfaches von acht ist, wird das letzte Byte mit 0 aufgefüllt.
Pro Bildzeile benötigt man (Breit + 7) / 8 Bytes. Man betrachtet dabei nur das Ganzzahlige Ergebnis. Im Beispiel mit 13 Bildpunkten pro Zeile sind das 20/8, also 2 Bytes
Als C-Code würde das Beispiel dann so aussehen:
const uint8_t PROGMEM smiley [26] =
{0x07, 0x00,
0x18, 0xc0,
0x20, 0x20,
0x59, 0x90,
0x59, 0x90,
0x80, 0x08,
0x80, 0x08,
0x90, 0x48,
0x48, 0x90,
0x47, 0x10,
0x20, 0x20,
0x18, 0xc0,
0x07, 0x00};
Mit der Funktion
void drawBitmap(int16_t x, int16_t y, const uint8_t bitmap[], int16_t w, int16_t h, uint16_t color, uint16_t bg);
wird das Bild bitmap mit der Breite w und der Höhe h an der Position x, y ausgegeben. Alle Pixel mit dem Wert 1 erhalten die Farbe color, alle anderen die Farbe bg. Wird der Parameter bg weggelassen, werden Pixel mit dem Wert 0 nicht ausgegeben, der Hintergrund bleibt erhalten.
8-Bit pro Bildpunkt:
Dieses Format benötigt ein Byte pro Pixel. Der Wert bestimmt die Helligkeit des Pixels. 0 = schwarz, 255 = weiß. Die Werte dazwischen geben entsprechende Grautöne.
Die Funktion
void drawGrayscaleBitmap(int16_t x, int16_t y, const uint8_t bitmap[], int16_t w, int16_t h);
gibt das Bild bitmap mit der Breite w und der Höhe h an der Position x, y in Grautönen aus.
Mit ein wenig eigener Programmierung kann das Bild auch als Abstufung einer beliebigen Farbe an Stelle von Grautönen ausgegeben werden. Hier die Funktion dazu:
void drawMonochromeBitmap(int16_t x, int16_t y, const uint8_t bitmap[],
int16_t w, int16_t h, uint16_t color)
{
float v; //to hold the brightness of a pixel as a floatingpoint number
uint16_t r,g,b; //to hold the required color as red, green and blue values
b = (color << 3) & 0xF8; //split the color in 565 formatt blue
g = (color >> 3) & 0xFC; //green and
r = (color >> 8) & 0xF8; //red
tft->startWrite(); //Start a SPI write stream
for (int16_t j = 0; j < h; j++, y++) //iterate over rows
{
for (int16_t i = 0; i < w; i++) //iterate over columns
{
v = 1.0 * pgm_read_byte(&bitmap[j * w + i]) /255;
//get the brightness of the pixel from image data and
//set the pixel on display using red, green and blue weighted by the
//brightness of image pixel
tft->writePixel(x + i, y, tft->color565(v * r, v * g, v * b));
}
}
tft->endWrite(); //stop the SPI write stream
}
Die Farbe wird zuerst in Ihre Bestandteile blau, grün und rot zerlegt. In zwei verschachtelten Schleifen für Zeilen und Spalten werden die Helligkeitswerte der Pixel aus dem Programmspeicher gelesen. Durch die Division durch 255 werden Werte zwischen 0.0 und 1.0 gebildet. Die Multiplikation mit 1.0 ist wichtig. Damit wird sichergestellt, dass die folgenden Rechenoperationen mit Fließkommaarithmetik durchgeführt werden. Die Farbe des Pixels wird dann aus den gewichteten Farbteilen zusammengesetzt.
16-Bit pro Bildpunkt:
Dieses Format benötigt zwei Bytes pro Pixel. Der Wert bestimmt die Farbe des Pixels im 565-Format.
Die Funktion
void draw16bitRGBBitmap(int16_t x, int16_t y, const uint16_t bitmap[], int16_t w, int16_t h);
gibt das Bild bitmap mit der Breite w und der Höhe h an der Position x, y aus.
Für das 16-Bit Format gibt es noch eine zweite Funktion, bei der ein zusätzliches 1-Bit Maskenbitmap steuert, ob ein Pixel ausgegeben wird oder nicht. Beide Bitmaps müssen die gleiche Breite und Höhe haben.
Die Funktionvoid draw16bitRGBBitmapWithMask(int16_t x, int16_t y, uint16_t *bitmap, uint8_t *mask, int16_t w, int16_t h);
gibt das Bild bitmap an Position x und y mit der Breite w und der Höhe h aus. Dabei wird die Maske mask berücksichtigt. Nur Pixel deren Bit in der Maske gesetzt sind, werden angezeigt.
Hintergrundbild:
Wenn man ein Hintergrundbild verwendet, muss das Hintergrundbild zuerst neu ausgegeben werden, ehe man Änderungen im Vordergrund durchführt. Das kann bei großen Hintergrundbildern zu Flackern führen. Eine Lösung dafür ist es, nur jenen Teil des Hintergrundbildes neu darzustellen der von der Änderung betroffen ist. Die folgende Funktion macht genau das.
void draw16bitRGBBitmapClipping(int16_t x, int16_t y, const uint16_t bitmap[], int16_t w, int16_t h, int16_t wb, int16_t xb, int16_t yb)
{
tft->startWrite(); //Start a SPI write stream
for (int16_t j = 0; j < h; j++) //iterate over rows
{
for (int16_t i = 0; i < w; i++) //iterate over columns
{
//set target pixel with color from pixel within image part
tft->writePixel(x + xb + i, y + yb + j, bitmap[(y+j) * wb + x +i]);
}
}
tft->endWrite(); //Stop a SPI write stream
}
Die Parameter x und y bestimmen die Position und die Parameter w und h die Größe des auszugebenden Teilbildes. Der Parameter bitmap zeigt auf das gesamte Hintergrundbild mit der Breite wb. Mit xb und yb wird die Position berücksichtigt, an der das Hintergrundbild ausgegeben wurde. Meistens ist das 0,0.
Der Link verweist auf ein Video, das eine Biene zeigt, die vor einem Hintergrundbild mit Blumen hin und her fliegt. Die Realisierung erfolgte mit obiger Funktion. Sie ist auch im Beispielprogramm zu diesem Beitrag enthalten. Link zum Video.
Beispielprogramm:
Das Beispielprogramm wurde für das 240 x 280 Display mit ST7789 Controller erstellt, kann aber, wie im Teil 1 gezeigt wurde, einfach für andere Displays geändert werden.
Das Beispielprogramm wird über den seriellen Monitor gesteuert. Die folgenden Kommandos sind möglich:
a Ein Blumenbild wird als 1-Bit Grafik in Gelb mit blauem Hintergrund dargestellt.
b Dasselbe Blumenbild wird als 8-Bit Grafik mit Graustufen dargestellt.
c Dasselbe Blumenbild wird als 8-Bit Grafik mit Abtönungen von gelb dargestellt.
d Dasselbe Blumenbild wird als 16-Bit Grafik bunt dargestellt.
e Dasselbe Blumenbild wird maskiert mit den Buchstaben „AZ“ dargestellt.
f Im Vordergrund des Blumenbildes bewegt sich eine kleine Biene.
Beginnt das Kommando mit einer Ziffer, so kann die Ausgabe von Text in einer Box getestet werden. Die erste Ziffer bestimmt die horizontale Ausrichtung (0 = links, 1 = mittig, 2 = rechts).
Die zweite Ziffer bestimmt die vertikale Ausrichtung (0 = oben, 1 = mittig, 2 = unten).
Alle nachfolgenden Zeichen werden von UTF8 auf Codepage 1250 umgewandelt und entsprechend der angegebenen Ausrichtung in der Box angezeigt.
Das ZIP-File mit dem Beispielprogramm sollte in das Arduino Sketchverzeichnis expandiert werden, Es enthält folgende Dateien:
gfx-teil2_test.ino Sketch mit dem Beispielprogramm.
1-helper.ino Hilfsfunktionen, die in diesem Beitrag beschrieben wurden.
Bunte_Seerosen_klein_1bit.h Bild mit Seerosen 240 x 180 im 1-bit/Pixel Format.
Bunte_Seerosen_klein_8bit.h Bild mit Seerosen 240 x 180 im 8-bit/Pixel Format.
Bunte_Seerosen_klein_16bit.h Bild mit Seerosen 240 x 180 im 16-bit/Pixel Format.
maske.h Maske mit den Zeichen „AZ“ 240 x 180 im 1-bit/Pixel Format.
biene_color.h Bild mit Biene 64 x 44 im 16-bit/Pixel Format.
biene_maske.h Maske mit Biene 64 x 44 im 1-bit/Pixel Format.
Arial_Rounded.h Schrift mit einer Höhe von 22pt .
Die Bilder und die Schrift wurden mit dem Programm GFX-Tool erstellt.
Viel Spaß beim Experimentieren mit Schriften und Bildern.
2 Reacties
Bernd-Steffen
Hallo Jürgen, ich bin an der XCL-Tabelle interessiert. mailto:berndsteffen@gmx.de Viele Grüße Bernd-Steffen
Jürgen
Hallo.
Ich habe mir eine Excel-Tabelle entworfen mit der man sich eigene Zeichen entwerfen kann.
Wer daran interessiert ist kann sich gerne an mich wenden. Ich sende ihm dann gerne diese Excel-Tabelle zu.