본문 바로가기

PyQt GUI

PyQt GUI (15) 리스트 아이템(list item) 드래그-앤-드롭(drag and drop) 이용하기

728x90

드래그-앤-드롭 Drag-and-Drop은 GUI에서 매우 많이 사용되는 기능입니다. 선택 가능한 리스트 중에서, 원하는 아이템을 고르는데 주로 사용됩니다. 이번 포스팅에서는 리스트 아이템에 드래그-앤-드롭 기능을 추가해 보도록 하겠습니다. 

리스트 위젯 QListWidget() 2개를 만들었습니다. 첫번째 리스트 위젯은 선택할 수 있는 아이템을 보여주는 리스트 위젯이고, 두번째 리스트 위젯은 선택하는 아이템을 담는 리스트 위젯 입니다. 즉 위에 있는 아이템 리스트를 드래그-앤-드롭하여 아래에 있는 리스트 위젯에 담으려고 하는 것 입니다. 주로 "보기" 중에서 몇 개를 (순서대로) "선택" 하는 경우에 위와 같은 UI를 구성할 것 입니다. 

 

리스트에는 색깔을 나타내는 Red, Green, Blue가 있는데 단순히 텍스트로만 Red, Green, Blue라고 써 놓으면 가시성이 별로 좋지 않기 때문에 텍스트 앞에 색깔을 나타내는 이미지를 추가하였습니다. 

 

리스트 위젯을 생성하는 방법, 리스트 위젯에 리스트 위젯 아이템 QListWidgetItem()을 생성하는 방법에 대해서는 이미 지난 포스팅에서 소개한 바 있습니다. 

https://studyingrabbit.tistory.com/19?category=957111 

 

PyQt GUI (1) : PyQt GUI Hello World

https://www.youtube.com/watch?v=U_vWfzhWINw PyQt를 이용한 GUI 만들기 첫 시간입니다. GUI에서 가장 많이 사용하는 위젯인 Label : 가장 간단하게 Text를 출력하는 위젯 PushButton : 버튼 위젯 ComboBox : 여..

studyingrabbit.tistory.com

PyQt GUI 에 대한 첫 번째 포스팅이었는데, 위 포스팅에서는 단순히 텍스트 형태의 아이템을 리스트에 추가하였습니다. 이번 포스팅에서는 색깔을 나타내는 이미지를 아이콘으로 만들어서, 아이콘도 추가할 것 입니다. 위 UI를 얻기 위한 전체 코드는 아래와 같습니다. 

import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class Main(QDialog):
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        layout = QVBoxLayout()

        label_example = QLabel("Select your favorite color")
        
        red_icon = QIcon("./red.png")
        green_icon = QIcon("./green.png")
        blue_icon = QIcon("./blue.png")

        red_item = QListWidgetItem(red_icon, "Red")
        green_item = QListWidgetItem(green_icon, "Green")
        blue_item = QListWidgetItem(blue_icon, "Blue")

        self.list_example = QListWidget()
        self.list_example.addItem(red_item)
        self.list_example.addItem(green_item)
        self.list_example.addItem(blue_item)

        label_favorite = QLabel("Drag-and-drop above items to below")
        
        self.list_favorite = QListWidget()

        layout.addWidget(label_example)
        layout.addWidget(self.list_example)
        layout.addWidget(label_favorite)
        layout.addWidget(self.list_favorite)
        self.setLayout(layout)
        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = Main()
    sys.exit(app.exec_())

맨 위, 맨 아래는 항상 보던 코드이니 중요하지 않고, 리스트와 리스트 아이템을 생성하는 코드의 핵심은

red_icon = QIcon("./red.png")
red_item = QListWidgetItem(red_icon, "Red")

self.list_example = QListWidget()
self.list_example.addItem(red_item)

와 같습니다. 첫 번째 줄은 QIcon()을 만들어 주는 부분인데, "./red.png"라는 파일을 아이콘화 하였습니다. red.png파일은 빨간색 사각형 이미지 입니다. 

 

두 번째 줄에서는 위에서 생성한 QIcon과 "Red"라는 텍스트를 QListWidgetItem()으로 만드는 부분입니다. QListWidgetItem()은 2개의 변수 red_icon과 "Red"를 인수로 받는데, 아이콘이 없는 경우에는 그냥 "Red"만 입력하면 평범한 텍스트 형태의 "Red"라는 문구가 리스트에 추가됩니다. 

 

세 번째 줄은 QListWidget() 을 생성하는 부분이며, 네 번째 줄은 앞에서 생성한 QListWidgetItem()을 QListWidget()에 추가하는 부분입니다. Red뿐 아니라, Green과 Blue도 똑같이 반복하여 세 QListWidgetItem()을 QListWidget()에 추가하였습니다. 첫 번째 리스트를 생성하였습니다. 

 

아래쪽에 있는 두 번째 QListWidget()인 self.list_favorite은 그냥 빈 리스트입니다. 위 리스트의 아이템을 이 리스트로 드래그-앤-드롭하여 리스트를 채울 예정입니다. 

 

그냥 이 상태에서는 위 리스트에 있는 아이템을 드래그 하지도 못 하고, 아래에 있는 리스트로 드롭하지도 못 합니다. 그냥 위에 있는 리스트의 아이템을 선택하는 것이 기능의 전부입니다. 왜냐하면 각 리스트 아이템에 드래그나, 드롭을 할 수 있도록 세팅하지 않았기 때문인데요, 다름과 같은 라인을 추가하여 리스트의 아이템을 드래그 혹은 드롭할 수 있도록 설정할 수 있습니다. 

self.list_example.setDragEnabled(True)
self.list_favorite.setAcceptDrops(True)

위 두 라인을 self.list_example과 self.list_favorite을 생성한 다음 추가해 주면 됩니다. 

 

setDragEnabled(True)는 해당 리스트위젯의 아이템을 드래그할 수 있도록 설정하는 것이고, sefAcceptDrops는 해당 리스트위젯에다가 드롭할 수 있도록 설정하는 것 입니다. 위 리스트에서 선택하여 드래그 하여, 아래 리스트로 드롭하는 것이 우리의 목표이기 때문에 위와 같이 설정해 주면 됩니다. 제대로 작성했다면, 위에 있는 리스트를 드래그 하여 아래에 있는 리스트로 드롭 할 수 있습니다.

추가적으로 리스트 위젯의 아이템의 텍스트, 즉 위 예시에서는 "Red", "Green", "Blue"라는 문구, 를 QLineEdit()이나 QPushButton()으로 드래그-앤-드롭 하는 것을 만들어 보도록 하겠습니다. 위에서 한 것과 같이 리스트에서 리스트로 아이템을 옮길 수 도 있지만, 리스트에서 LineEdit으로 아이템을 옮길 수 도 있습니다. 예를들면 위 예시 UI의 LineEdit에 색상을 영어로 타이핑을 해야하는데, 사람이 타이핑을 하는 경우 어떤 값을 입력해야 할지 모르거나, 입력하면서 오타를 칠 수 있기 때문에, 미리 예시 아이템을 만들어 놓고 그것을 드래그-앤-드롭을 하여 편히 입력하는 것 입니다. 

lineedit = QLineEdit()
layout.addWidget(lineedit)

우선 위와 같이 QLineEdit()을 하나 생성하고, 이것을 메인 layout에 추가합니다. 두 번째 리스트위젯의 바로 아래에 추가하였습니다. 이 상황에서 리스트위젯의 아이템을 드래그-앤-드롭을 하여 라인에딧에 드롭하여고 하면 되지 않음을 확인할 수 있습니다. 역시 라인에딧에 드롭을 할 수 있는 기능을 추가하지 않았기 때문입니다. 

lineedit.setAcceptDrops(True)

와 같이 추가하여 lineedit이 드롭을 받아 들일 수 있게 하면 될 것 같은데, 이렇게 하면 되지 않습니다. 왜냐하면, 라인에딧에는 텍스트 형태의 변수가 입력 될 수 있는데, 우리가 드래그한 것은 텍스트가 아니라 "리스트위젯아이템"이기 때문입니다. 따라서, 라인에딧에 리스트위젯아이템이 드롭된 경우, 리스트위젯아이템의 텍스트만을 받아서 이 텍스트를 라인에딧에 입력해야 합니다. 즉, 리스트위젯아이템은 현재 아이콘과 텍스트로 구성돼 있는데, 이 중에서 텍스트 부문만을 사용할 것 입니다. 따라서 이 기능을 할 수 있도록 라인에딧에 설정을 해 주어야 합니다. 

 

단순한 방법으로는 위 기능을 구현할 수 없고, 위 기능이 구현되도록 라인에딧 클래스를 새롭게 생성해 주어야 합니다. 아래와 같이 하면 됩니다. 

class QLineEdit(QLineEdit):
    def __init__(self):
        super(QLineEdit, self).__init__()
        self.setAcceptDrops(True)

    def dragEnterEvent(self, e):
        if e.mimeData().hasFormat("application/x-qabstractitemmodeldatalist"):
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        self.setText(e.source().currentItem().text())

위와 같이 기존의 QLineEdit()을 상속받아서 메소드 오버라이딩(override)하였습니다. 기본적인 QLineEdit()의 일부 기능을 수정하는 것 입니다. QLineEditCustom과 같이 새로운 이름의 클래스를 만들어도 되지만, 그냥 QLineEdit이라는 동일한 이름을 사용하였습니다. 

 

dragEnterEvent는 QLineEdit에 어떠한 객체가 드롭이 되었을 때, 어떠한 액션을 해야하는지에 대한 정의를 새롭게 하였습니다. 드롭되는 객체의 형식이 "application/x-qabstractitemmodeldatalist" 인 경우에는 진행을 하고, 그렇지 않은 경우에는 이 이벤트를 무시하는 것 입니다. 

 

dropEvent에서는 드롭이 되는 아이템을 어떻게 사용할 수 있는지에 대한 액션을 설정하였습니다. currentItem().text()는 현재 선택된 리스트의 아이템의 텍스트를 반환하는 함수입니다. 즉 리스트위젯아이템에서 "Red", "Green", "Blue"와 같이 텍스트 부분만을 따와서 QLineEdit에 setText를 하라는 설정입니다. 

 

위와 같이 QLineEdit을 오버라이딩하면 기존에 만들어 두었던 QLineEdit에 리스트위젯아이템을 드래그-앤-드롭할 수 있습니다. 

위 이미지에서는 Blue를 아래에 있는 리스트에 드롭하였고, Red를 라인에딧에 드롭하였습니다. 

 

이제는 리스트위젯아이템을 QPushButton에 드롭하여 버튼의 텍스트를 바꿔 보도록 하겠습니다. 위 라인에딧의 경우와 동일한데, 

class QPushButton(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, e):
        if e.mimeData().hasFormat("application/x-qabstractitemmodeldatalist"):
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        self.setText(e.source().currentItem().text())

와 같이 오버라이딩을 해 주면 됩니다. 물론 새로운 클래스를 만들어도 됩니다. 위 리스트의 아이템을 드래그 하여 버튼에 드롭하면 버튼의 이름이 "Red", "Green", "Blue"로 바꿔지게 됩니다. 이 역시, 버튼의 이름을 변경하고 싶을 때, 직접 타이핑을 하는 것 보다는 예제 중에서 하나를 선택할 수 있도록 하고 싶을 때 사용할 수 있습니다. 

 

아래에 Remove, Apply 버튼은 추가 기능을 수행할 수 있도록 추가한 것인데, 별로 어렵지 않아서 설명은 생략하도록 하겠습니다. 위 UI이 전체 코드는 아래와 같습니다. 

import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class QPushButton(QPushButton):
    def __init__(self, parent = None):
        super().__init__(parent)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, e):
        if e.mimeData().hasFormat("application/x-qabstractitemmodeldatalist"):
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        self.setText(e.source().currentItem().text())


class QLineEdit(QLineEdit):
    def __init__(self):
        super(QLineEdit, self).__init__()
        self.setAcceptDrops(True)

    def dragEnterEvent(self, e):
        if e.mimeData().hasFormat("application/x-qabstractitemmodeldatalist"):
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        self.setText(e.source().currentItem().text())


class Main(QDialog):
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        layout = QVBoxLayout()

        label_example = QLabel("Select your favorite color")
        label_favorite = QLabel("Drag-and-drop above items to below")

        red_icon = QIcon("./red.png")
        green_icon = QIcon("./green.png")
        blue_icon = QIcon("./blue.png")

        red_item = QListWidgetItem(red_icon, "Red")
        green_item = QListWidgetItem(green_icon, "Green")
        blue_item = QListWidgetItem(blue_icon, "Blue")

        self.list_example = QListWidget()
        self.list_example.addItem(red_item)
        self.list_example.addItem(green_item)
        self.list_example.addItem(blue_item)

        self.list_favorite = QListWidget()

        self.list_example.setDragEnabled(True)
        self.list_favorite.setAcceptDrops(True)

        lineedit = QLineEdit()
        button_name_change = QPushButton("Color")

        button_remove = QPushButton("Remove")
        button_apply = QPushButton("Apply")

        button_remove.clicked.connect(self.remove_selected_item)

        button_layout = QHBoxLayout()
        button_layout.addWidget(button_remove)
        button_layout.addWidget(button_apply)

        layout.addWidget(label_example)
        layout.addWidget(self.list_example)
        layout.addWidget(label_favorite)
        layout.addWidget(self.list_favorite)
        layout.addWidget(lineedit)
        layout.addWidget(button_name_change)
        layout.addLayout(button_layout)
        self.setLayout(layout)
        self.show()


    def remove_selected_item(self):
        reply = QMessageBox.question(self, "Question", "Are you sure to remove selected item?", QMessageBox.Yes | QMessageBox.No,
                                           QMessageBox.No)
        if reply == QMessageBox.Yes:
            for item in self.list_favorite.selectedItems():
                row = self.list_favorite.row(item)
                self.list_favorite.takeItem(row)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = Main()
    sys.exit(app.exec_())

 

728x90