지난시간에는 Text input rect를 만들었었습니다. 이번시간에는 Slider를 만들어 보겠습니다.
Slider 만들기
Silder는 시작과 끝이 있고, 그 사이에 Toggle이 있습니다.
Toggle을 마우스나 키보드를 활용해서 수직이나 수평으로 이동해서 값을 조절합니다.
Silder의 시작과 끝은 Silder가 가질 수 있는 최대값과 최소값을 의미하며
그 사이의 값은 Toggle의 위치에 비례합니다.
이 내용을 가지고 Slider를 만들어 보겠습니다.
class SlideBar:
_ACTIVE_COLOR = (192, 64, 64)
_DEACTIVE_COLOR = (192, 192, 192)
BACKGROUND_COLOR_MAP = {True: _ACTIVE_COLOR, False: _DEACTIVE_COLOR}
TOGGLE_COLOR = (64, 64, 64)
def __init__(self, x, y, w, h, min_value, max_value):
self.x, self.y, self.w, self.h = x, y, w, h
self.min_value, self.max_value = min_value, max_value
self.curr_value = min_value
self.toggle_x = x
self.toggle_y = y
self.toggle_width = w / 10
self.toggle_height = h
self.background_rect = self.get_background_rect()
self.toggle_rect = self.get_toggle_rect()
self.value_toggle_ratio = (self.max_value - self.min_value) / (self.w - self.toggle_width)
self.active: bool = False
self.dragging: bool = False
def get_background_rect(self) -> pg.Rect:
return pg.Rect(self.x, self.y, self.w, self.h)
def get_toggle_rect(self) -> pg.Rect:
return pg.Rect(self.toggle_x, self.toggle_y, self.toggle_width, self.toggle_height)
def update(self, surface):
pg.draw.rect(surface, self.BACKGROUND_COLOR_MAP[self.active], self.get_background_rect())
pg.draw.rect(surface, self.TOGGLE_COLOR, self.get_toggle_rect())
Slider의 pixel 크기 값과 실제 값을 변환해 줄 수 있는 self.value_toggle_ratio가 앞으로 toggle의 위치에 따른 값을 표현하는데 많은 도움이 될 것입니다.
그리고 이 Silder를 SubSettingDisplay에 작성해 줍시다.
class SubSettingDisplay:
## 생략 ##
def __init__(self, width, height):
self.width, self.height = width, height
self.full_height = height * 1.2
self.display_surface = None
self.title_font = pg.font.SysFont("calibri", 28)
self.contents_font = pg.font.SysFont("calibri", 16)
self.input_font = pg.font.SysFont("calibri", 16)
## 생략 ##
self.slider_sound_volume = SlideBar(
width * self.SYSTEM_LABEL_W,
height * self.CONTENTS_H * 1,
width * self.INPUT_W,
height * self.INPUT_H,
0, 100,
)
self.slider_preview_count = SlideBar(
width * self.SYSTEM_LABEL_W,
height * self.CONTENTS_H * 2,
width * self.INPUT_W,
height * self.INPUT_H,
1, 6,
)
self.slider_lock_delay = SlideBar(
width * self.SYSTEM_LABEL_W,
height * self.CONTENTS_H * 5,
width * self.INPUT_W,
height * self.INPUT_H,
0, 100,
)
self.slider_gravity = SlideBar(
width * self.SYSTEM_LABEL_W,
height * self.CONTENTS_H * 6,
width * self.INPUT_W,
height * self.INPUT_H,
0, 100,
)
self.slider_ARE = SlideBar(
width * self.SYSTEM_LABEL_W,
height * self.CONTENTS_H * 7,
width * self.INPUT_W,
height * self.INPUT_H,
0, 100,
)
self.slider_DAS = SlideBar(
width * self.SYSTEM_LABEL_W,
height * self.CONTENTS_H * 8,
width * self.INPUT_W,
height * self.INPUT_H,
0, 100,
)
self.slider_ARR = SlideBar(
width * self.SYSTEM_LABEL_W,
height * self.CONTENTS_H * 9,
width * self.INPUT_W,
height * self.INPUT_H,
0, 100,
)
self.slider_SDF = SlideBar(
width * self.SYSTEM_LABEL_W,
height * self.CONTENTS_H * 10,
width * self.INPUT_W,
height * self.INPUT_H,
0, 100,
)
def update(self):
## 생략 ##
self.input_right_rect.update(self.display_surface)
self.input_left_rect.update(self.display_surface)
self.input_CW_rotation_rect.update(self.display_surface)
self.input_CCW_rotation_rect.update(self.display_surface)
self.input_soft_drop_rect.update(self.display_surface)
self.input_hard_drop_rect.update(self.display_surface)
self.input_hold_rect.update(self.display_surface)
self.slider_sound_volume.update(self.display_surface)
self.slider_preview_count.update(self.display_surface)
self.slider_lock_delay.update(self.display_surface)
self.slider_gravity.update(self.display_surface)
self.slider_ARE.update(self.display_surface)
self.slider_DAS.update(self.display_surface)
self.slider_ARR.update(self.display_surface)
self.slider_SDF.update(self.display_surface)
여기까지 작성 후 실행하면 다음과 같은 화면을 확인할 수 있습니다.
슬라이더에 있는 배경요소와 toggle 요소의 height가 동일하고 margin등이 설정이 되어 있지 않아 디자인적으로 완성된 Slider는 볼 수 없었습니다만, 그래도 화면에 뜬 것은 확인 되었습니다. 이제 마우스 Event를 적용시켜 보겠습니다.
마우스 Event 만들기
이번에는 Mouse Control을 동작하도록 만들어 보겠습니다. 그럴려면 우선 toggle을 클릭했는지, Slider를 클릭했는지 판별할 수 있어야 하는데, pygame rect는 collidepoint라는 method로 지원하고 있습니다.
이 내용을 기반으로 Silder의 event_handler를 작성해 보겠습니다.
def event_handler(self, event: pg.event.Event):
## dragging event
if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
self.active = True if self.background_rect.collidepoint(event.pos) else False
self.dragging = True if self.toggle_rect.collidepoint(event.pos) else False
elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
self.dragging = False
elif event.type == pg.MOUSEMOTION and self.dragging:
self.toggle_x = min(self.x + self.w - self.toggle_width,
max(self.x, event.pos[0] - self.toggle_width/2))
self.curr_value = (self.toggle_x - self.x) * self.value_toggle_ratio + self.min_value
self.toggle_rect = self.get_toggle_rect()
그리고 이 event_handler를 SubSettingDisplay에 적용해 보겠습니다.
def event_handler(self, event):
self.input_right_rect.event_handler(event)
self.input_left_rect.event_handler(event)
self.input_CW_rotation_rect.event_handler(event)
self.input_CCW_rotation_rect.event_handler(event)
self.input_soft_drop_rect.event_handler(event)
self.input_hard_drop_rect.event_handler(event)
self.input_hold_rect.event_handler(event)
self.slider_sound_volume.event_handler(event)
self.slider_preview_count.event_handler(event)
self.slider_lock_delay.event_handler(event)
self.slider_gravity.event_handler(event)
self.slider_ARE.event_handler(event)
self.slider_DAS.event_handler(event)
self.slider_ARR.event_handler(event)
self.slider_SDF.event_handler(event)
이제 실행시키면 다음과 같이 동작하는 Slider를 확인할 수 있습니다.
하지만 slider가 너무 부드러운 것도 문제입니다. 현재 Preview Count는 1~6까지로 범위를 줬는데 1, 2, 3, 4, 5, 6과 같은 단계로 끊어져서 움직이는 것이 아니라 pixel별로 움직이고 있습니다.
이러한 문제를 해결하기 위해서는 toggle_x를 강제적으로 Value의 단계별로 끊어줘야 합니다. Quantize(양자화) 옵션을 만들어 주도록 합시다.
먼저 Slider에 단위 unit을 만들어 줍시다. 사실상 value_toggle_ratio의 역수입니다.
self.toggle_unit_step = (self.w - self.toggle_width) / (self.max_value - self.min_value)
그 다음에는 Quantize method를 만듭니다. +0.5를 하는 이유는 toggle unit 단위로 반올림을 해야 하기 때문입니다. +0.5가 없으면 버림처럼 계산이 됩니다.
def quantize(self, x_pos) -> float:
return self.toggle_unit_step * (x_pos // self.toggle_unit_step + 0.5)
그리고 이 값을 event.pos에 적용시켜보겠습니다.
def event_handler(self, event: pg.event.Event):
## dragging event
if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
self.active = True if self.background_rect.collidepoint(event.pos) else False
self.dragging = True if self.toggle_rect.collidepoint(event.pos) else False
elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
self.dragging = False
elif event.type == pg.MOUSEMOTION and self.dragging:
quantize_x_pos = self.quantize(event.pos[0] - self.toggle_width/2)
self.toggle_x = min(self.right_boundary, max(self.left_boundary, quantize_x_pos))
self.curr_value = (self.toggle_x - self.x) * self.value_toggle_ratio + self.min_value
self.toggle_rect = self.get_toggle_rect()
그럼 다음과 같이 동작합니다.
이번에는 Keyboard event를 처리해보도록 하겠습니다.
Keyboard Event 처리하기
keyboard event로 처리해야 할 것은 방향키 좌우 2개 입니다. 이 방향키에 맞춰서 슬라이더는 최소부터 최대까지 칸에 맞춰서 움직여야 합니다.
Keyboard Event를 만들어 줍시다.
def event_handler(self, event: pg.event.Event):
## dragging event
if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
self.active = True if self.background_rect.collidepoint(event.pos) else False
self.dragging = True if self.toggle_rect.collidepoint(event.pos) else False
elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
self.dragging = False
elif event.type == pg.MOUSEMOTION and self.dragging:
quantize_x_pos = self.quantize(event.pos[0] - self.toggle_width/2)
self.toggle_x = self.check_boundary(quantize_x_pos)
## keyboard event
if event.type == pg.KEYDOWN and self.active:
if event.key == pg.K_LEFT:
self.toggle_x = self.check_boundary(self.toggle_x - self.toggle_unit_step)
elif event.key == pg.K_RIGHT:
self.toggle_x = self.check_boundary(self.toggle_x + self.toggle_unit_step)
## update value, toggle rect
self.curr_value = (self.toggle_x - self.x) * self.value_toggle_ratio + self.min_value
self.toggle_rect = self.get_toggle_rect()
그러면 다음과 같이 동작합니다.
마지막으로, Slider의 데이터를 표현해 줄 Label을 만들어 줄 차례입니다. 이 영역은 사실 Slider가 할 일은 아니지만 외부에서 사용하기 좋게 Setting하고, 실제 외부에서는 어떻게 사용해야 하는지를 보여드리겠습니다.
Python Property
python의 Property는 Class에 있는 변수들의 getter, setter를 쉽게 선언할 수 있도록 도와주는 데코레이터 입니다.
python에서 class의 변수권한이 따로 나눠져 있지 않고 전부 public으로 선언 되어 있기 때문에 (underscore "_"로 명시적 구분은 할 수 있습니다) 이런 변수 조회 권한에 대해서 내부 동작을 가리기 위한 캡슐화를 돕는 python 기법이라 생각하시면 됩니다.
class SlideBar:
_ACTIVE_COLOR = (192, 64, 64)
_DEACTIVE_COLOR = (192, 192, 192)
BACKGROUND_COLOR_MAP = {True: _ACTIVE_COLOR, False: _DEACTIVE_COLOR}
TOGGLE_COLOR = (64, 64, 64)
def __init__(self, x, y, w, h, min_value: int, max_value: int):
self.x, self.y, self.w, self.h = x, y, w, h
self._min_value, self._max_value = min_value, max_value
self._curr_value = min_value
self._toggle_x = x
self._toggle_y = y
self._toggle_width = w / 10
self._toggle_height = h
self._background_rect = self._get_background_rect()
self._toggle_rect = self._get_toggle_rect()
self._value_toggle_ratio = (self._max_value - self._min_value) / (self.w - self._toggle_width)
self._toggle_unit_step = (self.w - self._toggle_width) / (self._max_value - self._min_value)
self._left_boundary = self.x
self._right_boundary = self.x + self.w - self._toggle_width
self._active: bool = False
self._dragging: bool = False
def _quantize(self, x_pos) -> float:
return self._toggle_unit_step * (x_pos // self._toggle_unit_step + 0.5)
def _check_boundary(self, x_pos) -> float:
return min(self._right_boundary, max(self._left_boundary, x_pos))
def _get_background_rect(self) -> pg.Rect:
return pg.Rect(self.x, self.y, self.w, self.h)
def _get_toggle_rect(self) -> pg.Rect:
return pg.Rect(self._toggle_x, self._toggle_y, self._toggle_width, self._toggle_height)
def update(self, surface):
pg.draw.rect(surface, self.BACKGROUND_COLOR_MAP[self._active], self._get_background_rect())
pg.draw.rect(surface, self.TOGGLE_COLOR, self._get_toggle_rect())
def event_handler(self, event: pg.event.Event):
## dragging event
if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
self._active = True if self._background_rect.collidepoint(event.pos) else False
self._dragging = True if self._toggle_rect.collidepoint(event.pos) else False
elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
self._dragging = False
elif event.type == pg.MOUSEMOTION and self._dragging:
quantize_x_pos = self._quantize(event.pos[0] - self._toggle_width/2)
self._toggle_x = self._check_boundary(quantize_x_pos)
## keyboard event
if event.type == pg.KEYDOWN and self._active:
if event.key == pg.K_LEFT:
self._toggle_x = self._check_boundary(self._toggle_x - self._toggle_unit_step)
elif event.key == pg.K_RIGHT:
self._toggle_x = self._check_boundary(self._toggle_x + self._toggle_unit_step)
## update value, toggle rect
self._curr_value = (self._toggle_x - self.x) * self._value_toggle_ratio + self._min_value
self._toggle_rect = self._get_toggle_rect()
@property
def value(self):
return self._curr_value
Slider를 사용할 준비가 끝났습니다. 이제 해당하는 value를 출력하여 Label의 text의 값으로 옮기면 됩니다. 이 작업은 SubSettingDisplay에서 이루어집니다.
def update(self):
## text_slider updater
self.ts_sound_volume_surface, self.ts_sound_volume_dest = get_text_info(str(self.slider_sound_volume.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 1.5, "l", "c")
self.ts_preview_count_surface, self.ts_preview_count_dest = get_text_info(str(self.slider_preview_count.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 2.5, "l", "c")
self.ts_lock_delay_surface, self.ts_lock_delay_dest = get_text_info(str(self.slider_lock_delay.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 5.5, "l", "c")
self.ts_gravity_surface, self.ts_gravity_dest = get_text_info(str(self.slider_gravity.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 6.5, "l", "c")
self.ts_ARE_surface, self.ts_ARE_dest = get_text_info(str(self.slider_ARE.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 7.5, "l", "c")
self.ts_DAS_surface, self.ts_DAS_dest = get_text_info(str(self.slider_DAS.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 8.5, "l", "c")
self.ts_ARR_surface, self.ts_ARR_dest = get_text_info(str(self.slider_ARR.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 9.5, "l", "c")
self.ts_SDF_surface, self.ts_SDF_dest = get_text_info(str(self.slider_SDF.value), self.contents_font, self.LABEL_COLOR, self.width * self.SLIDER_LABEL_W, self.height * self.CONTENTS_H * 10.5, "l", "c")
그러면 다음과 같이 동작하게 됩니다.
아무래도 경계조건이 제대로 만들어 지지 않은 듯 합니다. quantize 부분을 신경써서 다시 만들어 주도록 합시다.
def _quantize(self, x_pos) -> float:
return round((x_pos - self._x)/self._toggle_unit_step) * self._toggle_unit_step
그리고 handler에서도 적용하고 self._curr_value에 round도 적용해 봅시다.
def event_handler(self, event: pg.event.Event):
## dragging event
if event.type == pg.MOUSEBUTTONDOWN and event.button == pg.BUTTON_LEFT:
self._active = True if self._background_rect.collidepoint(event.pos) else False
self._dragging = True if self._toggle_rect.collidepoint(event.pos) else False
elif event.type == pg.MOUSEBUTTONUP and event.button == pg.BUTTON_LEFT:
self._dragging = False
elif event.type == pg.MOUSEMOTION and self._dragging:
self._toggle_x = self._check_boundary(self._x + self._quantize(event.pos[0] - self._toggle_width/2))
## keyboard event
if event.type == pg.KEYDOWN and self._active:
if event.key == pg.K_LEFT:
self._toggle_x = self._check_boundary(self._toggle_x - self._toggle_unit_step)
elif event.key == pg.K_RIGHT:
self._toggle_x = self._check_boundary(self._toggle_x + self._toggle_unit_step)
## update value, toggle rect
self._curr_value = round((self._toggle_x - self._x) * self._value_toggle_ratio + self._min_value)
self._toggle_rect = self._get_toggle_rect()
이 상태로 code를 실행하면 다음과 같이 동작합니다.
이제 Slider의 기능을 확실하게 만들었으니, Slider의 형태를 좀 더 보기 쉽게 만들어 줍시다. 다양한 상수를 넣어서 background rect와 width 속성을 바꿔줍시다.
class SlideBar:
_ACTIVE_COLOR = (192, 64, 64)
_DEACTIVE_COLOR = (192, 192, 192)
BACKGROUND_COLOR_MAP = {True: _ACTIVE_COLOR, False: _DEACTIVE_COLOR}
TOGGLE_COLOR = (64, 64, 64)
def __init__(self, x, y, w, h, min_value: int, max_value: int,
background_height_reduce_ratio=0.4,
toggle_height_reduce_ratio=0.2,
width_reduce_ratio=0.2,
toggle_ratio=0.1):
self._width_reduce_ratio = width_reduce_ratio ## ratio : 0 ~ 1
self._toggle_ratio = toggle_ratio ## ratio : 0 ~ 1
self._toggle_height_reduce_ratio = toggle_height_reduce_ratio ## ratio : 0 ~ 1
self._background_height_reduce_ratio = background_height_reduce_ratio ## ratio : 0 ~ 1
self._width_reduce = h * width_reduce_ratio
self._background_height = h * background_height_reduce_ratio
self._toggle_reduce_height = h * toggle_height_reduce_ratio
self._x = x + self._width_reduce
self._y = y + self._background_height
self._w = w - 2 * self._width_reduce
self._h = h - 2 * self._background_height
self._min_value, self._max_value = min_value, max_value
self._curr_value = min_value
self._toggle_x = self._x
self._toggle_y = y + self._toggle_reduce_height
self._toggle_width = w / 10
self._toggle_height = h - 2 * self._toggle_reduce_height
self._background_rect = self._get_background_rect()
self._toggle_rect = self._get_toggle_rect()
self._value_toggle_ratio = (self._max_value - self._min_value) / (self._w - self._toggle_width)
self._toggle_unit_step = (self._w - self._toggle_width) / (self._max_value - self._min_value)
self._left_boundary = self._x
self._right_boundary = self._x + self._w - self._toggle_width
self._active: bool = False
self._dragging: bool = False
이렇게 적용하면 이제 다음과 같은 화면을 얻을 수 있습니다.
슬슬 코드가 많아지고 있습니다. 이쯤되면 git으로 관리할 때가 된 듯 합니다.
다음시간에는 Check box를 만들어 보는 시간을 가지겠습니다.
* reference
'Development > Tetris' 카테고리의 다른 글
Python - 테트리스 만들기 Code 공유 (마지막) (2) | 2023.07.09 |
---|---|
Python - 테트리스(Tetris) 만들기 (16) - Checkbox (0) | 2023.04.02 |
Python - 테트리스(Tetris) 만들기 (14) - text input (0) | 2023.04.01 |
Python - 테트리스(Tetris) 만들기 (13) - Setting Screen (0) | 2023.03.27 |
Python - 테트리스(Tetris) 만들기 (12) - Main Screen (0) | 2023.03.26 |