music player를 만들어 보고 있는데, QSlider가 의외로 기능이 부실해서 그대로 쓸수가 없었다. 미디어 플레이어들을 보면, slider에 클릭 한번으로 위치를 이동하고, 드래그도 부드럽게 되는걸 볼 수 있다. 반면, 기본 QSlider는 임의 위치 클릭이 page 단위 이동을 의미하고, 슬라이더 바를 드래그 할 수 있지만, 자연스럽지 않게 tick 단위로 움직인다. 이를 보완하며 알게된 점을 기록해본다.
Qt Designer에 custom widget 사용하기
위젯의 기능을 확장하고 싶은데, ui 코드를 Qt Designer로 생성하고 있으니, 난감한 상황이 생겼다. Qt Designer에서 custom widget을 사용할 수 있나 찾아봤는데, 역시나 방법이 있었다. 관련정보는 다음 링크를 참고하였다. https://www.learnpyqt.com/courses/qt-creator/embed-pyqtgraph-custom-widgets-qt-app/
우선 사용할 클래스를 간단하게라도 만들어준다. 이미 만들어져 있다면 상관없다. 여기서는 QSlider를 상속받아 정의하였다.
class BTMPSlider(QSlider):
pass
Qt Designer에서 기본 위젯으로 자리를 잡고, 해당 위젯을 RMB 클릭하면 “다음으로 승격”이란 메뉴가 있다. 이걸 선택하면 커스텀 위젯을 지정할 수 있는 대화창이 뜬다.

이미지는 이미 승격된 슬라이더를 표시하고 있다. 새로 만드는 경우, 아래쪽 ‘새 승격된 클래스’ 부분을 채워주고 승격시키면 가능하다.
승격된 클래스 이름에는 내가 정의한 클래스 명을 사용하고, 헤더 파일에는 클래스가 들어있는 임포트할 파일 이름을 써준다. 아마도, 클래스 이름을 적으면 자동으로 “*.h” 헤더파일이 들어갈텐데, 해당부분을 지우고 내 파일이름을 적는다. 이는 Qt가 C++기반이기 때문인데, python의 경우 변환해주면 import 부분을 채워준다. 이미 입력된 부분을 보면, BTMPSlider, ui.btmpslider 로 이름이 들어간걸 볼 수 있다.
이렇게 승격된 상태로 pyside2-uic로 변환을 시키면, 다음처럼 코드에 들어간걸 확인 가능하다.
from ui.btmpslider import BTMPSlider
...
self.MusicProgressSlider = BTMPSlider(self.centralwidget)
...
Event Handler 추가하기
QSlider에 추가할 기능은 Mouse click과 Drag 기능이다. 이를 구현하기 위해 BTMPSlider 클래스에 mousePressEvent, mouseMoveEvent, mouseReleaseEvent를 오버라이드 한다. 구현 내용은 다음과 같다.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_drag: bool = False
def mousePressEvent(self, ev: QMouseEvent):
print("mouse press")
if ev.button() == Qt.LeftButton:
ev.accept()
x = ev.pos().x()
value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
self.setValue(int(value))
else:
return super().mousePressEvent(ev)
def mouseMoveEvent(self, ev: QMouseEvent):
print("mouse move")
if ev.buttons() & Qt.LeftButton:
ev.accept()
x = ev.pos().x()
value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
self.setValue(int(value))
if not self._is_drag:
self._is_drag = True
else:
return super().mouseMoveEvent(ev)
def mouseReleaseEvent(self, ev: QMouseEvent):
print("mouse release")
print(ev.button())
if ev.button() == Qt.LeftButton and self._is_drag:
ev.accept()
self._is_drag = False
x = ev.pos().x()
value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
else:
return super().mouseReleaseEvent(ev)
단순 클릭인 경우, LMB를 체크하여, 위치정보를 value값으로 환산해 설정해준다.
마우스가 움직일 때, 버튼이 클릭되어 있으면 is_drag를 설정하고 릴리즈될 때 이를 해제해줘 drag 상태를 구현했다. 주의할점은, mouseMoveEvent()에서 mouse button 상태정보가 ev.button()이 아니라 ev.buttons()라는 점이다. 전자는 이벤트를 발생시키는 버튼을 알려주며, 후자는 현재 버튼의 상태들이 or 연산으로 겹쳐있어 이중 LMB가 눌렸는지 bit and 연산으로 확인해주고 있다.
Custom Signal 추가하기
QSlider에 추가한 기능들에 대해 이벤트가 발생할 때, 뮤직 플레이어의 재생위치를 변경해야 하므로, 처리가 가능하도록 이벤트에 대한 시그널을 추가해준다.
Signal을 만들려고 이거저거 좀 찾아봤는데, 오히려 혼란만 주는 문서만 찾았다. 다른건 신경쓸게 없고, class 의 멤버로 만들어야 하는 것만 주의하면 된다.
class BTMPSlider(QSlider):
mouseClick: Signal = Signal(int)
dragStart: Signal = Signal(int)
dragEnd: Signal = Signal(int)
이제 원하는 위치에서 해당 signal.emit() 을 호출하여 시그널을 보내면 완료. 이를 추가한 전체 코드는 다음과 같다.
from PySide2.QtCore import Signal
from PySide2.QtGui import QMouseEvent, Qt
from PySide2.QtWidgets import QSlider
class BTMPSlider(QSlider):
mouseClick: Signal = Signal(int)
dragStart: Signal = Signal(int)
dragEnd: Signal = Signal(int)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_drag: bool = False
def mousePressEvent(self, ev: QMouseEvent):
print("mouse press")
if ev.button() == Qt.LeftButton:
ev.accept()
x = ev.pos().x()
value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
self.setValue(int(value))
self.mouseClick.emit(value)
else:
return super().mousePressEvent(ev)
def mouseMoveEvent(self, ev: QMouseEvent):
print("mouse move")
if ev.buttons() & Qt.LeftButton:
ev.accept()
x = ev.pos().x()
value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
self.setValue(int(value))
if not self._is_drag:
self._is_drag = True
self.dragStart.emit(value)
else:
return super().mouseMoveEvent(ev)
def mouseReleaseEvent(self, ev: QMouseEvent):
print("mouse release")
print(ev.button())
if ev.button() == Qt.LeftButton and self._is_drag:
ev.accept()
self._is_drag = False
x = ev.pos().x()
value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
self.dragEnd.emit(value)
else:
return super().mouseReleaseEvent(ev)
클라이언트 코드에서 다음과 같이 시그널에 메소드 함수들을 연결해 사용한다.
...
self.ui.MusicProgressSlider.mouseClick.connect(self.set_play_pos)
self.ui.MusicProgressSlider.dragStart.connect(self.drag_start)
self.ui.MusicProgressSlider.dragEnd.connect(self.drag_end)
...