본문 바로가기
PyQt5

QTableWidget 실행취소&다시실행 구현하기

by Tripleler 2022. 5. 30.

TableWidget은 Qt기반의 스프레드 시트입니다.

 

다만 정말 기본적인 표 기능만 하기 때문에 세부적인 작동은 직접 정의해주어야 합니다.

이번 포스팅에서는 엑셀처럼 실행취소&다시실행(이하 undo&redo) 기능을 구현해보고자 합니다.

 

 

 

 

undo와 redo를 구현하기 위해서는 우선 큐의 개념을 알아둘 필요가 있습니다.

간단하게 큐는 데이터를 순서대로 저장하고, 빼오는 것을 말합니다.

구현하기 위한 조건을 정리해봅시다.

 

1. 내용이 수정될 때마다 수정되기 전 데이터를 순서대로 undo큐에 저장합니다.

undo가 요청되면 현 상태를 redo큐에 저장합니다.

가장 최근에 저장된 순서로 undo큐에서 데이터를 꺼내옵니다.

 

2. redo가 요청되면 다시 현 상태를 undo큐에 저장하고,

가장 최근에 저장된 순서로 redo큐에서 데이터를 꺼내옵니다.

 

3. undo가 실행된 상태에서 redo큐에 데이터가 남아 있을 때,

redo가 아닌 데이터 수정이 가해지면 redo큐는 비워주어야 합니다.

 

 

 

 

이제 코드로 기본적인 테이블 위젯과 undo&redo버튼을 구현해봅시다.

 

import sys
from PyQt5.QtWidgets import *
import pandas as pd
from collections import deque


class MyApp(QWidget):

    def __init__(self):
        super().__init__()

        header = ['a', 'b', 'c', 'd']
        self.table = QTableWidget()
        self.table.setRowCount(20)
        self.table.setColumnCount(4)
        self.table.setHorizontalHeaderLabels(header)
        k = 1
        df_list = []
        for i in range(20):
            df_list2 = []
            for j in range(4):
                self.table.setItem(i, j, QTableWidgetItem(str(k)))
                df_list2.append(str(k))
                k += 1
            df_list.append(df_list2)
        self.data = pd.DataFrame(df_list, columns=header)
        self.table.itemChanged.connect(self.change)

        self.btn_undo = QPushButton(self)
        self.btn_undo.setIcon(app.style().standardIcon(QStyle.SP_ArrowLeft))
        self.btn_undo.setEnabled(False)
        self.btn_undo.clicked.connect(self.undo)
        self.undo_list = deque(maxlen=5)
        
        self.btn_redo = QPushButton(self)
        self.btn_redo.setIcon(app.style().standardIcon(QStyle.SP_ArrowRight))
        self.btn_redo.setEnabled(False)
        self.btn_redo.clicked.connect(self.redo)
        self.redo_list = deque()

        layout = QGridLayout()
        layout.addWidget(self.btn_undo, 0, 0, 1, 1)
        layout.addWidget(self.btn_redo, 0, 1, 1, 1)
        layout.addWidget(self.table, 1, 0, 10, 2)
        self.setLayout(layout)

        self.setWindowTitle('undo&redo')
        self.setGeometry(300, 100, 600, 400)
        self.show()

    def undo(self):
        pass

    def redo(self):
        pass

    def change(self):
        pass

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MyApp()
    sys.exit(app.exec_())

버튼 두 개와 스프레드시트를 만들었습니다.

또한 작업내용을 저장하기 위해 pandas라이브러리를 활용하여

현 스프레드시트 데이터를 데이터프레임으로 만들었습니다.

 

 

 

 

change함수를 구현해봅시다.

    def change(self, item):
        self.undo_list.append(self.data.copy())
        self.redo_list.clear()
        self.btn_undo.setEnabled(True)
        self.btn_redo.setEnabled(False)
        self.data.iloc[item.row(), item.column()] = item.text()

QTableWidget의 itemChanged 이벤트가 발생하면 change함수를 수행합니다.

현 상태를 undo큐에 저장하고, redo큐는 데이터가 섞이지 않도록 비워줍니다.

undo버튼활성화 및 redo버튼을 비활성화합니다.

마지막으로 데이터프레임과 스프레드시트를 동기화합니다.

a열 1행의 데이터를 바꾸자 undo버튼이 활성화되었습니다.

 

 

 

 

이제 undo를 구현합시다.

    def undo(self):
        if len(self.undo_list):
            self.redo_list.append(self.data.copy())
            self.btn_redo.setEnabled(True)
            self.table.blockSignals(True)
            self.data = self.undo_list.pop()
            rows, cols = self.data.shape
            for row in range(rows):
                for col in range(cols):
                    self.table.setItem(row, col, QTableWidgetItem(str(self.data.iloc[row, col])))
            self.table.blockSignals(False)
            self.btn_redo.setEnabled(True)
            if not len(self.undo_list):
                self.btn_undo.setEnabled(False)

undo행위 자체가 이전 데이터를 불러오면서 QTableWidget의 itemChaged 이벤트가 발생합니다.

이는 무한루프에 빠지게 될 수 있으므로 undo가 실행되는 동안

blockSignals(True)를 통해 이벤트 발생을 막아둡니다.

 

 

 

 

마지막 redo 입니다.

    def redo(self):
        if len(self.redo_list):
            self.undo_list.append(self.data.copy())
            self.btn_undo.setEnabled(True)
            self.table.blockSignals(True)
            self.data = self.redo_list.pop()
            rows, cols = self.data.shape
            for row in range(rows):
                for col in range(cols):
                    self.table.setItem(row, col, QTableWidgetItem(str(self.data.iloc[row, col])))
            self.table.blockSignals(False)
            self.btn_redo.setEnabled(True)
            if not len(self.redo_list):
                self.btn_redo.setEnabled(False)

undo와 반대 기능을 하므로 크게 다르지 않습니다.

 

 

 

이상으로 실행취소&다시실행(redo&undo) 기능을 구현해 보았습니다.

댓글