Skip to content

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 되었는지 확인

if imgui.is_mouse_hovering_rect(*roi):
    # TODO ...

영역(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 환경변수 설정하면 된다:

import os
os.environ["SDL_VIDEO_X11_FORCE_EGL"] = "1"

See also

Favorite site