Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team
Feb 19th, 2026
Timothy @ Daangn Frontend Core
Software Engineer, Frontend @Daangn Frontend Core Team
ex) Senior Lead Software Engineer @NHN Dooray (2021 ~ 2023)
ex) Lead Software Engineer @ProtoPie (2016 ~ 2021)
ex) Microsoft MVP
이 웅재
글자마다 별도의 상자(케이스)에 모았음
대문자용 활자 케이스는 upper-case, 소문자용 활자 케이스는 lower-case
이러한 케이스들의 집합을 폰트라고 함
여러 크기의 폰트를 모아 타입이라 함
글자의 굵기나 기울기 변형은 타입의 페이스라 함
스타일과 크기를 의미함
bold, normal, weight, font-family
Tk 의 폰트 객체는 고정된 크기, 스타일, 글자 두께 같은 정보를 담고 있습니다.
import tkinter
import tkinter.font
if __name__ == "__main__":
window = tkinter.Tk()
bi_times = tkinter.font.Font(
family="Times",
size=16,
weight="bold",
slant="italic"
)
canvas = tkinter.Canvas(window, width=WIDTH, height=HEIGHT)
canvas.pack()
canvas.create_text(200, 100, text="Hello, World!", font=bi_times)
tkinter.mainloop().metrics: 세로축에 대한 정보
linespace: 텍스트의 높이
ascent: 기준선 윗부분
descent: 기준선 아래부분
크기가 다른 글자가 같은 줄에 있을때 기준선으로 정렬해야 함
fixed: 글자마다의 가로 너비가 항상 같은지 여부
font-size 는 픽셀이 아니라 포인트 (약 1/72 인치)
print(bi_times.metrics())
{'ascent': 15, 'descent': 4, 'linespace': 19, 'fixed': 0}.measure: 가로축에 대한 정보
전체 픽셀값을 반올림해서 보여주기 때문에 합이 다를 수 있음
일부 폰트는 kerning 를 이용해서 조합의 가로 값이 달라질 수 있음
print(bi_times.measure("Hi!"))
24
print(bi_times.measure("H"))
13
print(bi_times.measure("i"))
5
print(bi_times.measure("!"))
7if __name__ == "__main__":
window = tkinter.Tk()
canvas = tkinter.Canvas(window, width=WIDTH, height=HEIGHT)
canvas.pack()
font1 = tkinter.font.Font(
family="Times",
size=16,
slant="italic"
)
font2 = tkinter.font.Font(
family="Times",
size=16,
slant="italic"
)
x, y = 200, 200
canvas.create_text(x, y, text="Hello, ", font=font1)
x += font1.measure("Hello, ")
canvas.create_text(x, y, text="World!", font=font2)
tkinter.mainloop()if __name__ == "__main__":
window = tkinter.Tk()
canvas = tkinter.Canvas(window, width=WIDTH, height=HEIGHT)
canvas.pack()
font1 = tkinter.font.Font(
family="Times",
size=16,
slant="italic"
)
font2 = tkinter.font.Font(
family="Times",
size=16,
slant="italic"
)
x, y = 200, 200
canvas.create_text(x, y, text="Hello, ", font=font1)
x += font1.measure("Hello, ")
canvas.create_text(x, y, text="Overlapping!", font=font2)
tkinter.mainloop()if __name__ == "__main__":
window = tkinter.Tk()
canvas = tkinter.Canvas(window, width=WIDTH, height=HEIGHT)
canvas.pack()
font1 = tkinter.font.Font(
family="Times",
size=16,
slant="italic"
)
font2 = tkinter.font.Font(
family="Times",
size=16,
slant="italic"
)
x, y = 200, 200
canvas.create_text(x, y, text="Hello, ", font=font1, anchor="nw")
x += font1.measure("Hello, ")
canvas.create_text(x, y, text="Overlapping!", font=font2, anchor="nw")
tkinter.mainloop()def layout(text):
font = tkinter.font.Font() #
display_list = []
cursor_x, cursor_y = HSTEP, VSTEP
for word in text.split(): #
w = font.measure(word) #
display_list.append(
(cursor_x, cursor_y, word) #
)
cursor_x += w + font.measure(" ") #
if cursor_x + w > WIDTH - HSTEP: #
cursor_y += font.metrics("linespace") * 1.25 #
cursor_x = HSTEP
return display_list토큰으로 변경
토큰은 텍스트거나 태그
class Text:
def __init__(self, text):
self.text = text
class Tag:
def __init__(self, tag):
self.tag = tagdef lex(body):
out = [] #
buffer = "" #
in_tag = False
for c in body:
if c == "<":
in_tag = True
if buffer: #
out.append(Text(buffer)) #
buffer = "" #
elif c == ">":
in_tag = False
out.append(Tag(buffer)) #
buffer = "" #
else:
buffer += c #
if not in_tag and buffer: #
out.append(Text(buffer)) #
return out #def layout(tokens):
font = tkinter.font.Font()
display_list = []
cursor_x, cursor_y = HSTEP, VSTEP
weight = "normal"
style = "roman"
for tok in tokens:
if isinstance(tok, Text):
for word in tok.text.split():
font = tkinter.font.Font(
size=16,
weight=weight,
slant=style
)
w = font.measure(word)
display_list.append(
(cursor_x, cursor_y, word, font)
)
cursor_x += w + font.measure(" ")
if cursor_x + w > WIDTH - HSTEP:
cursor_y += font.metrics("linespace") * 1.25
cursor_x = HSTEP
elif tok.tag == "i":
style = "italic"
elif tok.tag == "/i":
style = "roman"
elif tok.tag == "b":
weight = "bold"
elif tok.tag == "/b":
weight = "normal"
return display_list def draw(self):
for x, y, c, font in self.display_list:
if y > self.scroll + HEIGHT:
continue
if y + VSTEP < self.scroll:
continue
self.canvas.create_text(x, y - self.scroll, text=c, font=font, anchor="nw")class Layout:
def __init__(self, tokens):
self.display_list = []
self.cursor_x = HSTEP
self.cursor_y = VSTEP
self.weight = "normal"
self.style = "roman"
for tok in tokens:
self.token(tok)class Layout:
def __init__(self, tokens):
def token(self, tok):
if isinstance(tok, Text):
for word in tok.text.split():
self.word(word)
elif tok.tag == "i":
self.style = "italic"
elif tok.tag == "/i":
self.style = "roman"
elif tok.tag == "b":
self.weight = "bold"
elif tok.tag == "/b":
self.weight = "normal"class Layout:
def __init__(self, tokens):
def token(self, tok):
def word(self, word):
font = tkinter.font.Font(
size=16,
weight=self.weight,
slant=self.style
)
w = font.measure(word)
self.display_list.append(
(self.cursor_x, self.cursor_y, word, font)
)
self.cursor_x += w + font.measure(" ")
if self.cursor_x + w > WIDTH - HSTEP:
self.cursor_y += font.metrics("linespace") * 1.25
self.cursor_x = HSTEPclass Browser:
#
def load(self, url):
body = url.request()
tokens = lex(body)
self.display_list = Layout(tokens).display_list
self.draw()class Layout:
def __init__(self, tokens):
self.display_list = []
self.cursor_x = HSTEP
self.cursor_y = VSTEP
self.weight = "normal"
self.style = "roman"
self.size = 12 #
for tok in tokens:
self.token(tok)
class Layout:
def token(self, tok):
if isinstance(tok, Text):
for word in tok.text.split():
self.word(word)
elif tok.tag == "i":
self.style = "italic"
elif tok.tag == "/i":
self.style = "roman"
elif tok.tag == "b":
self.weight = "bold"
elif tok.tag == "/b":
self.weight = "normal"
elif tok.tag == "small": #
self.size -= 2 #
elif tok.tag == "/small": #
self.size += 2 #
elif tok.tag == "big": #
self.size += 4 #
elif tok.tag == "/big": #
self.size -= 4 #
class Layout:
def word(self, word):
font = tkinter.font.Font(
size=self.size, #
weight=self.weight,
slant=self.style
)
w = font.measure(word)
self.display_list.append(
(self.cursor_x, self.cursor_y, word, font)
)
self.cursor_x += w + font.measure(" ")
if self.cursor_x + w > WIDTH - HSTEP:
self.cursor_y += font.metrics("linespace") * 1.25
self.cursor_x = HSTEP
class Layout:
def __init__(self, tokens):
self.display_list = []
self.cursor_x = HSTEP
self.cursor_y = VSTEP
self.weight = "normal"
self.style = "roman"
self.size = 12
self.line = [] #
for tok in tokens:
self.token(tok)
self.flush()class Layout:
def __init__(self, tokens):
self.display_list = []
self.cursor_x = HSTEP
self.cursor_y = VSTEP
self.weight = "normal"
self.style = "roman"
self.size = 12
self.line = [] #
for tok in tokens:
self.token(tok)
self.flush()class Layout:
def word(self, word):
font = tkinter.font.Font(
size=self.size,
weight=self.weight,
slant=self.style
)
w = font.measure(word)
if self.cursor_x + w > WIDTH - HSTEP: #
self.flush() #
self.line.append((self.cursor_x, word, font)) #
self.cursor_x += w + font.measure(" ")기준선을 따라 단어들을 정렬
디스플레이 리스트에 모든 단어들을 추가
cursor_x 와 cursor_y 필드를 업데이트
def flush(self):
if not self.line: return
metrics = [font.metrics() for x, word, font in self.line]
max_ascent = max([metric["ascent"] for metric in metrics])
baseline = self.cursor_y + 1.25 * max_ascent
for x, word, font in self.line:
y = baseline - font.metrics("ascent")
self.display_list.append((x, y, word, font))
max_descent = max([metric["descent"] for metric in metrics])
self.cursor_y = baseline + 1.25 * max_descent
self.cursor_x = HSTEP
self.line = [] def token(self, tok):
#
elif tok.tag == "br":
self.flush()
elif tok.tag == "/p":
self.flush()
self.cursor_y += VSTEP폰트를 글로벌 캐싱
FONTS = {}
def get_font(size, weight, style):
key = (size, weight, style)
if key not in FONTS:
font = tkinter.font.Font(size=size, weight=weight,
slant=style)
label = tkinter.Label(font=font)
FONTS[key] = (font, label)
return FONTS[key][0] def word(self, word):
font = get_font(self.size, self.weight, self.style) #
w = font.measure(word)
if self.cursor_x + w > WIDTH - HSTEP:
self.flush()
self.line.append((self.cursor_x, word, font))
self.cursor_x += w + font.measure(" ")By Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team