จากบทความการใช้งาน u8g2 ที่สามารถเรนเดอร์ (Render) ภาษาไทย (Thai string) ได้ผ่านทางฟังก์ชัน drawUTF8() ของไลบรารี u8g2 แต่การแสดงผลไม่ถูกต้อง ดังภาพที่ 1 ด้วยเหตุนี้จึงต้องปรับปรุงโค้ดของไลบรารีเพิ่มเติมเพื่อให้การแสดงผลถูกต้องดังภาพที่ 2
ปัญหาและแนวทางแก้ไขปัญหา
จากภาพที่ 1 จะพบว่าการแสดงผลของเครื่องหมายไม่ถูกต้อง เนื่องจากเครื่องหมายวรรณยุกต์์และสระบางตัวจะต้องอยู่ตำแหน่งเดียวกับพยัญชนะด้านหน้า ดังนั้น เราจะแก้ปัญหาด้วยการตรวจสอบตัวอักขระก่อนทำการแสดงผล เพื่อให้ได้ตำแหน่งการแสดงผลที่ถูกต้องออกมาดังภาพที่ 2
ฟังก์ชันที่เกี่ยวข้อง
ฟังก์ชันแสดงผลที่เรียกใช้ในตัวอย่างโปรแกรมคือ u8g2.drawUTF8() ซึ่งคำสั่งนี้ปรากฏอยู่ในไฟล์ u8g2lib.h และเขียนไว้ดังนี้
u8g2_uint_t drawUTF8(u8g2_uint_t x, u8g2_uint_t y, const char *s)
{
return u8g2_DrawUTF8(&u8g2, x, y, s);
}
นั่นหมายความว่ามีการเรียกใช้ u8g2_DrawUTF8() อีกทอดหนึง ซึ่งฟังก์ชันด้งกล่าวอยู่ในไฟล์ชื่อ u8g2_font.c ซึ่งอยู่ในโฟลเดอร์ csrc และเขียนโค้ดเอาไว้เป็นดังต่อไปนี้
u8g2_uint_t u8g2_DrawUTF8(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str)
{
u8g2->u8x8.next_cb = u8x8_utf8_next;
return u8g2_draw_string(u8g2, x, y, str);
}
จากโค้ดของ u8g2_DrawUTF8() มีคำสั่ง 2 คำสั่ง และพบว่ามีการเรียกใช้ u8g2_draw_string() ซึ่งเป็นฟังก์ชันเป้าหมายที่แท้จริงสำหรับการแก้ไขในครั้งนี้
u8g2_draw_string()
ภายใต้โค้ดของ u8g2_draw_string() จะมีโค้ดเขียนไว้ดังนี้
static u8g2_uint_t u8g2_draw_string(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str) U8G2_NOINLINE;
static u8g2_uint_t u8g2_draw_string(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str)
{
uint16_t e;
u8g2_uint_t delta, sum;
u8x8_utf8_init(u8g2_GetU8x8(u8g2));
sum = 0;
for(;;)
{
e = u8g2->u8x8.next_cb(u8g2_GetU8x8(u8g2), (uint8_t)*str);
if ( e == 0x0ffff )
break;
str++;
if ( e != 0x0fffe )
{
delta = u8g2_DrawGlyph(u8g2, x, y, e);
#ifdef U8G2_WITH_FONT_ROTATION
switch(u8g2->font_decode.dir)
{
case 0:
x += delta;
break;
case 1:
y += delta;
break;
case 2:
x -= delta;
break;
case 3:
y -= delta;
break;
}
/*
// requires 10 bytes more on avr
x = u8g2_add_vector_x(x, delta, 0, u8g2->font_decode.dir);
y = u8g2_add_vector_y(y, delta, 0, u8g2->font_decode.dir);
*/
#else
x += delta;
#endif
sum += delta;
}
}
return sum;
}
สิ่งที่เราจะต้องทำคือ ตรวจสอบว่า อักขระจากข้อมูล *str นั้นเป็นวรณณยุกต์และสระที่เราต้องการหรือไม่ เพื่อปรับค่า x ที่จะถูกส่งไปให้ฟังก์ชัน u8g2_DrawGlyph() เพื่อระบุตำแหน่งของแกน x สำหรับแสดงผล นอกจากนี้ พบว่าตัวแปร delta เป็นค่าความกว้างของตัวอักษรที่วาด ดังนั้น เพื่อความปลอดภัยควรประกาศให้ delta มีค่าเป็น 0 ในตอนประกาศตัวแปรดังนี้
u8g2_uint_t delta=0, sum;
หลังจากนั้นจะพบว่ามีการแปลงข้อมูลตัวอักษรให้เป็นรหัสแบบ Unicode ด้วยคำสั่ง ต่อไปนี้
e = u8g2->u8x8.next_cb(u8g2_GetU8x8(u8g2), (uint8_t)*str);
และทำการตรวจสอบว่า e เป็น 0x0ffff หรือไม่ ถ้าใช่จะออกจากการวาดอักขระ แต่ถ้า e ไม่เป็น 0x0fffe จะทำการวาดอักขระในบรรทัดคำสั่งต่อไปนี้
delta = u8g2_DrawGlyph(u8g2, x, y, e);
นั่นหมายความว่า เราต้องทำการตรวจสอบตัวอักษรที่จะส่งวาดก่อนเรียกคำสั่งด้านบน ดังนั้น จึงทำการตรวจสอบค่าของ *(str—) ว่าเป็นวรรณยุกต์หรือสระที่ต้องการตรวจสอบหรือไม่ ถ้าใช่ จะลดค่าของ x เท่ากับค่า delta โดยในโค้ดด้านล่างนี้ได้จัดเก็บรายการสระและวรรณยุกต์ที่ใช้ในการตรวจสอบเอาไว้ในตัวแปร t
...
uint8_t s;
uint8_t t[] = {
(uint8_t)'่',
(uint8_t)'้',
(uint8_t)'๊',
(uint8_t)'๋',
(uint8_t)'ุ',
(uint8_t)'ู',
(uint8_t)'ิ',
(uint8_t)'ี',
(uint8_t)'ึ',
(uint8_t)'ื',
(uint8_t)'ั'
};
...
for( ;; ) {
s = (uint8_t)*str;
e = u8g2->u8x8.next_cb(u8g2_GetU8x8(u8g2), s);
...
for (int i=0; i<11; i++) {
if (s == t[i]) {
x -= delta;
break;
}
}
delta = u8g2_DrawGlyph(u8g2, x, y, e);
...
}
ฟังก์ชันที่ถูกแก้ไขแล้ว
จากการหลักการทำงานที่ต้องการปรับปรุง ได้ทำการแก้ไขโค้ดของฟังก์ชัน u8g2_draw_string() ออกมาได้ดังนี้
static u8g2_uint_t u8g2_draw_string(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str) U8G2_NOINLINE;
static u8g2_uint_t u8g2_draw_string(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str)
{
uint16_t e;
u8g2_uint_t delta=0, sum;
u8x8_utf8_init(u8g2_GetU8x8(u8g2));
sum = 0;
uint8_t s;
uint8_t t[] = {
(uint8_t)'่',
(uint8_t)'้',
(uint8_t)'๊',
(uint8_t)'๋',
(uint8_t)'ุ',
(uint8_t)'ู',
(uint8_t)'ิ',
(uint8_t)'ี',
(uint8_t)'ึ',
(uint8_t)'ื',
(uint8_t)'ั'
};
for(;;)
{
s = (uint8_t)*str;
e = u8g2->u8x8.next_cb(u8g2_GetU8x8(u8g2), s);
if ( e == 0x0ffff )
break;
str++;
if ( e != 0x0fffe )
{
for (int i=0; i<11; i++) {
if (s == t[i]) {
x -= delta;
break;
}
}
delta = u8g2_DrawGlyph(u8g2, x, y, e);
#ifdef U8G2_WITH_FONT_ROTATION
switch(u8g2->font_decode.dir)
{
case 0:
x += delta;
break;
case 1:
y += delta;
break;
case 2:
x -= delta;
break;
case 3:
y -= delta;
break;
}
/*
// requires 10 bytes more on avr
x = u8g2_add_vector_x(x, delta, 0, u8g2->font_decode.dir);
y = u8g2_add_vector_y(y, delta, 0, u8g2->font_decode.dir);
*/
#else
x += delta;
#endif
sum += delta;
}
}
return sum;
}
ตัวอย่างโปรแกรม
โค้ดตัวอย่างโปรแกรมของบทความนี้เป็นดังนี้
#define U8G2_WITH_UNICODE
#include <Arduino.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
//U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
void setup() {
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_etl14thai_t );
u8g2.drawUTF8(10,10,"สวัสดี");
u8g2.drawUTF8(10,30,"ที่นี่ที่ไหน");
u8g2.drawUTF8(10,50,"มีใครอยู่ไหม?");
u8g2.sendBuffer();
}
void loop() {
}
สรุป
จากบทความนี้จะพบว่า เราสามารถแก้ปัญหาการแสดงภาษาไทยในไลบรารี u8g2 จากฟังก์ชัน drawUTF8() ได้ถูกต้องแล้ว โดยไลบรารียังคงทำงานได้อย่างถูกต้องและใช้ได้กับไมโครคอนโทรลเลอร์ ESP32, ESP8266 และ STM32F103 ได้ (ทางทีมงานเราเหลือให้ใช้งานอยู่เพียงเท่านี้) ส่วนชิพอื่น ๆ นั้นผู้อ่านต้องนำไปทดสอบและแก้ไขการทำงานกันต่อไป สุดท้ายนี้ จะพบว่า ถ้าเราเข้าใจรูปแบบของภาษา และขั้นตอนวิธีการทำงานของโค้ด เราสามารถปรับปรุงโค้ดได้ง่ายขึ้น ดังนั้น ขอให้สนุกกับการเขียนโปรแกรมครับ
(C) 2020-2022, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-11-16, 2022-01-19