Pyimgui
Cython-based Python bindings for dear imgui
직접 컴파일
# will install Cython as extra dependency and compile from Cython sources
pip install imgui[Cython] --no-binary imgui
# will compile from pre-generated C++ sources
pip install imgui --no-binary imgui
pygame Backend 선택시 imgui[pygame]
패키지를 선택하면 Extra Require 는 pygame으로 선택된다. pygame-ce를 선택하고 싶다면 수동으로 설치하자.
pygame Example
from __future__ import absolute_import
from imgui.integrations.pygame import PygameRenderer
import OpenGL.GL as gl
import imgui
import pygame
import sys
def main():
pygame.init()
size = 800, 600
pygame.display.set_mode(size, pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
imgui.create_context()
impl = PygameRenderer()
io = imgui.get_io()
io.display_size = size
show_custom_window = True
while 1:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit(0)
impl.process_event(event)
impl.process_inputs()
imgui.new_frame()
if imgui.begin_main_menu_bar():
if imgui.begin_menu("File", True):
clicked_quit, selected_quit = imgui.menu_item(
"Quit", "Cmd+Q", False, True
)
if clicked_quit:
sys.exit(0)
imgui.end_menu()
imgui.end_main_menu_bar()
imgui.show_test_window()
if show_custom_window:
is_expand, show_custom_window = imgui.begin("Custom window", True)
if is_expand:
imgui.text("Bar")
imgui.text_colored("Eggs", 0.2, 1.0, 0.0)
imgui.end()
# note: cannot use screen.fill((1, 1, 1)) because pygame's screen
# does not support fill() on OpenGL sufraces
gl.glClearColor(1, 1, 1, 1)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
imgui.render()
impl.render(imgui.get_draw_data())
pygame.display.flip()
if __name__ == "__main__":
main()
OpenGL.error.Error: Attempt to retrieve context when no valid context
에러가 발생된다면 아래의 트러블 슈팅 항목을 참조. 간단히, SDL_VIDEO_X11_FORCE_EGL=1
환경변수 설정하면 된다.
또한 pygame 렌더러 코드는 아래의 #PygameRenderer 항목 참조.
pygame 이미지를 로드하는 방법
import pygame
import imgui
from imgui.integrations.pygame import PygameRenderer
from OpenGL.GL import *
def load_texture(image_path):
# Load the image using pygame
image_surface = pygame.image.load(image_path)
image_data = pygame.image.tostring(image_surface, "RGBA", 1)
width, height = image_surface.get_size()
# Generate a new texture ID
texture_id = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, texture_id)
# Set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
# Upload texture data
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_data)
glBindTexture(GL_TEXTURE_2D, 0)
return texture_id, width, height
# Load the image and create a texture
image_path = "path/to/your/image.png" # Replace with your image path
texture_id, img_width, img_height = load_texture(image_path)
# ...
imgui.begin("Image Window")
imgui.image(texture_id, img_width, img_height) # Display the image
imgui.end()
위젯을 윈도우의 하단에 붙여서 출력
window_size = imgui.get_window_size()
cursor_pos = imgui.get_cursor_pos()
# input_field_height = (
# imgui.get_text_line_height_with_spacing()
# + imgui.get_style().FramePadding.y * 2
# )
input_field_height = imgui.get_frame_height()
y_offset = window_size.y - input_field_height - imgui.get_style().window_padding.y
imgui.set_cursor_pos_y(y_offset)
directory_text = self._open_file_popup_path
changed, directory_text = imgui.input_text("Directory", directory_text, -1)
한글폰트 설정
io = imgui.get_io()
font_path = str(get_fonts_path() / "NanumGothicCoding.ttf")
font_scaling_factor = 1
font_size_in_pixels = 14
font_size_pixels = font_size_in_pixels * font_scaling_factor
glyph_ranges = io.fonts.get_glyph_ranges_korean()
io.fonts.clear()
io.fonts.add_font_from_file_ttf(font_path, font_size_pixels, None, glyph_ranges)
io.font_global_scale /= font_scaling_factor
스타일 설정
imgui.push_style_color(imgui.COLOR_BUTTON, imgui.get_color_u32_rgba(0.5, 0.5, 0.5, 1.0))
imgui.push_style_color(imgui.COLOR_BUTTON_HOVERED, imgui.get_color_u32_rgba(0.5, 0.5, 0.5, 1.0))
imgui.push_style_color(imgui.COLOR_BUTTON_ACTIVE, imgui.get_color_u32_rgba(0.5, 0.5, 0.5, 1.0))
if imgui.button("Click Me"):
pass
imgui.pop_style_color(3) # 스타일을 복원합니다.
캔버스에 커서를 올리거나, 클릭한 상태를 감지하는 방법
invisible_button
을 사용하면 된다.
# Using `imgui.invisible_button()` as a convenience
# 1) it will advance the layout cursor and
# 2) allows us to use `is_item_hovered()`/`is_item_active()`
imgui.invisible_button("## CanvasButton", cw, ch, self.button_flags)
is_hovered = imgui.is_item_hovered()
is_active = imgui.is_item_active()
# ...
캔버스에서 마우스 특정영역 Hover 되었는지 확인
영역(ROI) 중심에 텍스트 그리기
def draw_centered_text(
draw_list: _DrawList,
label: str,
x1: float,
y1: float,
x2: float,
y2: float,
color=WHITE,
) -> Tuple[float, float, float, float]:
rect_width = x2 - x1
rect_height = y2 - y1
text_width, text_height = imgui.calc_text_size(label)
x = x1 + (rect_width - text_width) / 2.0
y = y1 + (rect_height - text_height) / 2.0
draw_list.add_text(x, y, color, label)
return x, y, x + text_width, y + text_height
Table 에서 Row 선택 하능하도록
imgui.SELECTABLE_SPAN_ALL_COLUMNS
를 사용하면 되는데 imgui.table_set_column_index(0)
으로 컬럼 위치를 지정해야 한다.
assert isinstance(df, pd.DataFrame)
page_data = df.iloc[start:end]
data_rows, data_cols = page_data.shape
table = imgui.begin_table("Table", data_cols + 1, TABLE_FLAGS)
if table.opened:
imgui.table_setup_column("Index") ##
for col in page_data.columns:
imgui.table_setup_column(col)
imgui.table_headers_row()
with table:
rows = page_data.itertuples(index=False)
for i, row in enumerate(rows, start=start):
imgui.table_next_row()
imgui.table_set_column_index(0)
selected = bool(i == self._selected_row)
if imgui.selectable(str(i), selected, imgui.SELECTABLE_SPAN_ALL_COLUMNS)[1]:
self._selected_row = i
for cell in row:
imgui.table_next_column()
imgui.text(str(cell))
Renderers
PygameRenderer
pygame 렌더러 구현 코드:
from __future__ import absolute_import
from .opengl import FixedPipelineRenderer
import pygame
import pygame.event
import pygame.time
import imgui
class PygameRenderer(FixedPipelineRenderer):
def __init__(self):
super(PygameRenderer, self).__init__()
self._gui_time = None
self.custom_key_map = {}
self._map_keys()
def _custom_key(self, key):
# We need to go to custom keycode since imgui only support keycod from 0..512 or -1
if not key in self.custom_key_map:
self.custom_key_map[key] = len(self.custom_key_map)
return self.custom_key_map[key]
def _map_keys(self):
key_map = self.io.key_map
key_map[imgui.KEY_TAB] = self._custom_key(pygame.K_TAB)
key_map[imgui.KEY_LEFT_ARROW] = self._custom_key(pygame.K_LEFT)
key_map[imgui.KEY_RIGHT_ARROW] = self._custom_key(pygame.K_RIGHT)
key_map[imgui.KEY_UP_ARROW] = self._custom_key(pygame.K_UP)
key_map[imgui.KEY_DOWN_ARROW] = self._custom_key(pygame.K_DOWN)
key_map[imgui.KEY_PAGE_UP] = self._custom_key(pygame.K_PAGEUP)
key_map[imgui.KEY_PAGE_DOWN] = self._custom_key(pygame.K_PAGEDOWN)
key_map[imgui.KEY_HOME] = self._custom_key(pygame.K_HOME)
key_map[imgui.KEY_END] = self._custom_key(pygame.K_END)
key_map[imgui.KEY_INSERT] = self._custom_key(pygame.K_INSERT)
key_map[imgui.KEY_DELETE] = self._custom_key(pygame.K_DELETE)
key_map[imgui.KEY_BACKSPACE] = self._custom_key(pygame.K_BACKSPACE)
key_map[imgui.KEY_SPACE] = self._custom_key(pygame.K_SPACE)
key_map[imgui.KEY_ENTER] = self._custom_key(pygame.K_RETURN)
key_map[imgui.KEY_ESCAPE] = self._custom_key(pygame.K_ESCAPE)
key_map[imgui.KEY_PAD_ENTER] = self._custom_key(pygame.K_KP_ENTER)
key_map[imgui.KEY_A] = self._custom_key(pygame.K_a)
key_map[imgui.KEY_C] = self._custom_key(pygame.K_c)
key_map[imgui.KEY_V] = self._custom_key(pygame.K_v)
key_map[imgui.KEY_X] = self._custom_key(pygame.K_x)
key_map[imgui.KEY_Y] = self._custom_key(pygame.K_y)
key_map[imgui.KEY_Z] = self._custom_key(pygame.K_z)
def process_event(self, event):
# perf: local for faster access
io = self.io
if event.type == pygame.MOUSEMOTION:
io.mouse_pos = event.pos
return True
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
io.mouse_down[0] = 1
if event.button == 2:
io.mouse_down[1] = 1
if event.button == 3:
io.mouse_down[2] = 1
return True
if event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
io.mouse_down[0] = 0
if event.button == 2:
io.mouse_down[1] = 0
if event.button == 3:
io.mouse_down[2] = 0
if event.button == 4:
io.mouse_wheel = .5
if event.button == 5:
io.mouse_wheel = -.5
return True
if event.type == pygame.KEYDOWN:
for char in event.unicode:
code = ord(char)
if 0 < code < 0x10000:
io.add_input_character(code)
io.keys_down[self._custom_key(event.key)] = True
if event.type == pygame.KEYUP:
io.keys_down[self._custom_key(event.key)] = False
if event.type in (pygame.KEYDOWN, pygame.KEYUP):
io.key_ctrl = (
io.keys_down[self._custom_key(pygame.K_LCTRL)] or
io.keys_down[self._custom_key(pygame.K_RCTRL)]
)
io.key_alt = (
io.keys_down[self._custom_key(pygame.K_LALT)] or
io.keys_down[self._custom_key(pygame.K_RALT)]
)
io.key_shift = (
io.keys_down[self._custom_key(pygame.K_LSHIFT)] or
io.keys_down[self._custom_key(pygame.K_RSHIFT)]
)
io.key_super = (
io.keys_down[self._custom_key(pygame.K_LSUPER)] or
io.keys_down[self._custom_key(pygame.K_LSUPER)]
)
return True
if event.type == pygame.VIDEORESIZE:
surface = pygame.display.get_surface()
# note: pygame does not modify existing surface upon resize,
# we need to to it ourselves.
pygame.display.set_mode(
(event.w, event.h),
flags=surface.get_flags(),
)
# existing font texure is no longer valid, so we need to refresh it
self.refresh_font_texture()
# notify imgui about new window size
io.display_size = event.size
# delete old surface, it is no longer needed
del surface
return True
def process_inputs(self):
io = imgui.get_io()
current_time = pygame.time.get_ticks() / 1000.0
if self._gui_time:
io.delta_time = current_time - self._gui_time
else:
io.delta_time = 1. / 60.
if(io.delta_time <= 0.0): io.delta_time = 1./ 1000.
self._gui_time = current_time
Troubleshooting
OpenGL.error.Error: Attempt to retrieve context when no valid context
Ubuntu 20.04 이상에서 Wayland 로 실행된다면 다음과 같은 에러가 출력될 수 있다:
pygame-ce 2.4.1 (SDL 2.28.5, Python 3.11.4)
/home/your/Project/ddrm/test.py:13: Warning: PyGame seems to be running through X11 on top of wayland, instead of wayland directly
pygame.display.set_mode(size, pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
Traceback (most recent call last):
File "/home/your/Project/ddrm/.venv/lib/python3.11/site-packages/OpenGL/latebind.py", line 43, in __call__
return self._finalCall( *args, **named )
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: 'NoneType' object is not callable
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/your/Project/ddrm/test.py", line 65, in <module>
main()
File "/home/your/Project/ddrm/test.py", line 59, in main
impl.render(imgui.get_draw_data())
File "/home/your/Project/ddrm/.venv/lib/python3.11/site-packages/imgui/integrations/opengl.py", line 303, in render
gl.glVertexPointer(2, gl.GL_FLOAT, imgui.VERTEX_SIZE, ctypes.c_void_p(commands.vtx_buffer_data + imgui.VERTEX_BUFFER_POS_OFFSET))
File "/home/your/Project/ddrm/.venv/lib/python3.11/site-packages/OpenGL/latebind.py", line 47, in __call__
return self._finalCall( *args, **named )
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/your/Project/ddrm/.venv/lib/python3.11/site-packages/OpenGL/wrapper.py", line 818, in wrapperCall
storeValues(
File "/home/your/Project/ddrm/.venv/lib/python3.11/site-packages/OpenGL/arrays/arrayhelpers.py", line 156, in __call__
contextdata.setValue( self.constant, pyArgs[self.pointerIndex] )
File "/home/your/Project/ddrm/.venv/lib/python3.11/site-packages/OpenGL/contextdata.py", line 58, in setValue
context = getContext( context )
^^^^^^^^^^^^^^^^^^^^^
File "/home/your/Project/ddrm/.venv/lib/python3.11/site-packages/OpenGL/contextdata.py", line 40, in getContext
raise error.Error(
OpenGL.error.Error: Attempt to retrieve context when no valid context
다음과 같이 SDL_VIDEO_X11_FORCE_EGL=1
환경변수 설정하면 된다: