본문 바로가기
프로그래밍 관련/PyQt

PyQt TableWidget (심화1)

by 존버매니아.임베디드 개발자 2021. 12. 3.
반응형
  • Selection Mode에 대하여
  • 현재 선택된 셀의 행번호 혹은 열번호 파악하기
  • 특정 셀 선택해서 삭제하기(문제점 설명)
  • 특정 셀 선택해서 삭제하기(해결책)
  • 특정 셀의 속성 지정하기 (수정불가, 사용불가, 선택불가,drag&drop 불가 등등) (flag와 연관됨)

Selection Mode에 대하여

마우스로 테이블을 선택했을 때, 선택되는 대상을 무엇으로 할지 고를 수 있다.

셀을 고르거나, 행을 고르거나, 열을 고르거나 할 수 있다. 아래 보면 이해 될거라 믿는다.

def selectionBehavior(self) -> 'QAbstractItemView.SelectionBehavior': ...
class SelectionBehavior(int):
        SelectItems = ... # type: QAbstractItemView.SelectionBehavior
        SelectRows = ... # type: QAbstractItemView.SelectionBehavior
        SelectColumns = ... # type: QAbstractItemView.SelectionBehavior

현재 선택된 셀의 행번호 혹은 열번호 파악하기

방법이 좀 여러가지 있는데 일단 아래 2개의 메소드를 사용할 수 있다.

※ return 타입이 list 인점에 유의하자.

def selectedItems(self) -> typing.List[QTableWidgetItem]: ...
def selectedIndexes(self) -> typing.List[QtCore.QModelIndex]: ...

위 메소드를 사용하여 QTableWidgetItem 객체 혹은 QModeIndex 정보를 전달 받은 후,

row() 메소드를 사용하면 선택된 셀의 row 번호가 출력된다.

column() 메소드를 사용하면 선택된 셀의 column 번호가 출력된다.

 

아래 예시 코드로 실험해 볼 것.

셀을 선택 한 후, 버튼을 누르면 선택된 셀들의 행번호, 열번호를 알려준다.

import sys
from PyQt5.QtWidgets import *

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setupUI()

    def setupUI(self):
        self.setGeometry(800, 200, 800, 300)       

        self.ta1 = QTableWidget(self)
        self.ta1.resize(400, 500)
        self.ta1.setColumnCount(3)       

        table_column=["첫번째 열" , "두번째 열" , "Third 열"]
        self.ta1.setHorizontalHeaderLabels(table_column)

        
        #행 2개 추가
        self.ta1.setRowCount(2)
        
        #추가된 행에 데이터 채워넣음
        self.ta1.setItem(0, 0, QTableWidgetItem("(0,0)"))
        self.ta1.setItem(0, 1, QTableWidgetItem("(0,1)"))
        self.ta1.setItem(1, 0, QTableWidgetItem("(1,0)"))
        self.ta1.setItem(1, 1, QTableWidgetItem("(1,1)"))
               

        self.btn1=QPushButton("Button1",self)
        self.btn1.move(400,5)
        self.btn1.clicked.connect(self.fun_btn1)
    
    def fun_btn1(self):
        for i in self.ta1.selectedItems():
            print("selectedItems 선택된 셀의 행번호 :%d" %i.row())
            print("selectedItems 선택된 셀의 행번호 :%d" %i.column())

        for i in self.ta1.selectedIndexes():
            print("selectedIndexes 선택된 셀의 행번호 :%d" %i.row())
            print("selectedIndexes 선택된 셀의 행번호 :%d" %i.column())

    
if __name__ == "__main__":
    app = QApplication(sys.argv)
    mywindow = MyWindow()
    mywindow.show()
    app.exec_()

※ 아래 2개 메소드의 차이

def selectedItems(self) -> typing.List[QTableWidgetItem]: ...
def selectedIndexes(self) -> typing.List[QtCore.QModelIndex]: ...

일단 QModeIndex가 정확히 뭔지 잘 모르겠는데,

이 2개의 동작상에 차이가 뭐가 있냐면

첫번째 selectedItems 메소드는 return 타입이 QTableWidgetItem 이다.

 

2021.12.02 - [프로그래밍 관련/PyQt] - PyQt. TableWidget(기본)

위 글에서  -아무 것도 추가하지 않은 비어있는 셀에 관해 유의할 점

에서 보면 

행은 추가했지만 QTableWidgetItem 을 아직 생성하지 않은 경우,

해당 셀은 리턴 할 QTableWidgetItem 객체가 아예 없는 상태다.

 

그러므로, 만약 비어있는 셀을 선택한 상태에서 selectedItems 메소드를 호출하면 None Class를 호출 하기 때문에

row() 메소드, colunm 메소드를 사용할 수가 없다.

 

반면, selectedIndexes 메소드의 경우 얘가 정확히 뭔진 모르겠는데 얘는 비어있는 셀을 선택해도

무언가가 리턴이 되서 row() 메소드 column 메소드 사용이 가능하다.

위 그림을 살펴보면 0행 2열의 경우 QTableWidgetItem이 없는 상태이고, 따라서

0행 2열을 고른 상태에서 버튼을 클릭하면

첫번째 print 문은 출력안되고 두번째 print 문만 출력되는 모습을 볼 수 있다.


특정 셀 선택해서 삭제하기(문제점 설명)

현재 선택된 셀의 행번호만 알 수 있다면 removeRow 메소드를 사용해서 삭제가 가능함

앞에서 현재 선택된 셀의 row 값을 받는 방법을 알아왔으니 

이걸 사용해서 삭제가 가능할 것이다.

 

※ 근데 여기서 이슈가 있다.

만약 선택된 셀이 1개인 경우에는 문제가 없다.

근데 선택된 셀이 여러개인 경우 신경써야 할 이슈가 있는데 왜 인지 예를 들어 설명해보겠다.

for i in self.ta1.selectedIndexes():
            self.ta1.removeRow(i.row())

위 코드를 해석해보자

현재 선택된 셀들의 Row 값을 받아와서

removeRow 메소드를 사용해서 해당 Row들을 삭제한다.

이렇게하면 끝날거 같은데.. 여기서 문제가 발생하는데 다음과 같은 상황을 생각해보자.

위 그림과 처럼 1,2번 행을 삭제하려고 한다.

따라서

removeRow(1)

removeRow(2)

를 하면 된다고 생각 될 것이다.

 

근데 여기서 문제가 뭐냐면

removeRow(1)이 실행되고 나면

위 그림처럼 된다.

원래 우리가 지우려고했던 2번행의 행 번호가 1번으로 바껴버린다.

그래서 removeRow(1)이 한번 실행된 상태에서

removeRow(2)를 실행하면 2번행은 이미 없는 상태이기 때문에 더 삭제가 안된다.


특정 셀 선택해서 삭제하기(해결책)

해결책은 2가지가 있다

1) 선택된 셀들의 행번호를 return 받은 후, row 번호가 큰 순서대로 삭제한다.

 -> 이게 왜 해결책이 되는지는 생각하기 바란다. 쓰기가 귀찮다. 힘내라 내 머리! 과거의 넌 이해했었어!

 

2) 원리를 명확하게는 모르겠는데 stack overflow에서 찾아낸 해결책이다.

1,2,3,4 행이 있을 때

예를 들어 2번행을 삭제하면 뒤에 있는 3,4번 행이 한칸씩 땡겨져서

3,4번 -> 2,3 번으로 수정되는 이런 현상 때문에 지금의 문제가 생기는 것인데

 

상세한 원리는 잘 모르겠다. 굳이 그거까지 공부하긴 귀찮고 그냥 아래 예시코드 고맙게 사용하자.

index_list = []                                                          
for model_index in self.ta1.selectedIndexes():
	index = QtCore.QPersistentModelIndex(model_index)         
	index_list.append(index)

for index in index_list:                                      
	self.ta1.removeRow(index.row())

특정 셀의 속성 지정하기 (수정불가, 사용불가, 선택불가 등등) (flag와 연관됨)

QTableWidgetItem 클래스의 아래 3가지와 연관이 있다.

def setFlags(self, aflags: typing.Union[QtCore.Qt.ItemFlags, QtCore.Qt.ItemFlag]) -> None: ...
def flags(self) -> QtCore.Qt.ItemFlags: ...
class ItemFlag(int):
        NoItemFlags = ... # type: Qt.ItemFlag
        ItemIsSelectable = ... # type: Qt.ItemFlag
        ItemIsEditable = ... # type: Qt.ItemFlag
        ItemIsDragEnabled = ... # type: Qt.ItemFlag
        ItemIsDropEnabled = ... # type: Qt.ItemFlag
        ItemIsUserCheckable = ... # type: Qt.ItemFlag
        ItemIsEnabled = ... # type: Qt.ItemFlag
        ItemIsTristate = ... # type: Qt.ItemFlag
        ItemNeverHasChildren = ... # type: Qt.ItemFlag
        ItemIsUserTristate = ... # type: Qt.ItemFlag
        ItemIsAutoTristate = ... # type: Qt.ItemFlag

 

item.flags().__index__()

를 사용하면 해당 셀에 셋팅된 flag 현황을 볼 수 있다.

위 ItemFlag 클래스에 대해 이야기하자면

flag에서 셋팅할 수 있는게 총 10개다. (NoItemFlag는 아무것도 안한다는 의미임)

만약 ItemIsSelectable 플래그를 set 했으면 해당 셀은 선택이 가능하다.

근데 이 flag를 만약 clear 해놨으면 해당 셀은 선택이 안된다.

 

만약 ItemIsDragEnabled  플래그를 set 했으면 해당 셀은 드래그 가능하다.

만약 해당 플래그를 clear 해놨으면 해당 셀은 드래그가 불가하다.

 

다른 flag들도 전부 같은 원리라고 생각하면 된다.

 

코드 예시를 들어설명함

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

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setupUI()

    def setupUI(self):
        self.setGeometry(800, 200, 800, 300)       

        self.ta1 = QTableWidget(self)
        self.ta1.resize(400, 500)
        self.ta1.setColumnCount(3)       

        table_column=["첫번째 열" , "두번째 열" , "Third 열"]
        self.ta1.setHorizontalHeaderLabels(table_column)      
        
        #행 2개 추가
        self.ta1.setRowCount(2)
        
        #추가된 행에 데이터 채워넣음
        self.ta1.setItem(0, 0, QTableWidgetItem("(0,0)"))
        self.ta1.setItem(0, 1, QTableWidgetItem("(0,1)"))
        self.ta1.setItem(1, 0, QTableWidgetItem("(1,0)"))
        self.ta1.setItem(1, 1, QTableWidgetItem("(1,1)"))

        item = self.ta1.item(1,1)
        print(item.flags().__index__())
        
if __name__ == "__main__":
    app = QApplication(sys.argv)
    mywindow = MyWindow()
    mywindow.show()
    app.exec_()

위 코드를 실행해보면

print(item.flags().__index__()) 에 의하여 
63이 출력된다.
63을 2진수로 바꾸면
1 1 1 1 1 1  (1이 6개)
이고, 이 말은
        ItemIsSelectable = ... # type: Qt.ItemFlag
        ItemIsEditable = ... # type: Qt.ItemFlag
        ItemIsDragEnabled = ... # type: Qt.ItemFlag
        ItemIsDropEnabled = ... # type: Qt.ItemFlag
        ItemIsUserCheckable = ... # type: Qt.ItemFlag
        ItemIsEnabled = ... # type: Qt.ItemFlag
        ItemIsTristate = ... # type: Qt.ItemFlag
        ItemNeverHasChildren = ... # type: Qt.ItemFlag
        ItemIsUserTristate = ... # type: Qt.ItemFlag
        ItemIsAutoTristate = ... # type: Qt.ItemFlag
여기서 위에서부터 순서대로 6개까지 flag가 set돼있다는 소리다.
 
따라서 선택 가능,수정가능, 드래그가능... 등등 다 가능하단 소리다.
 
만약 1행1열 셀을 수정불가하게 바꾸려면 어떻게해야될까?
현재 set되어있는 IsEditable flag를 clear하면된다.
 
비트연산을 적절하게 사용하면된다.
item.setFlags(item.flags() ^  QtCore.Qt.ItemFlag.ItemIsEditable)
위 코드를 실행하면
Editable flag만 clear가 된다.
이 코드가 실행되고나면 더 이상 해당 셀은 수정이 안된다.
 
나머지도 비트연산 적절히 사용해서 코드 짜면된다.
참고로,
IsEditable flag는 그 값이 2다.
 
원래 flag 값이 63이었는데 IsEditable flag만 clear 하면 그 값이 61이 될 것이다.
(1 1 1 1 1 1 -> 1 1 1 1 0 1 이 되니까)
 
아래 예시코드 보고 파악할 것
import sys
from PyQt5.QtWidgets import *
from PyQt5 import QtCore

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setupUI()

    def setupUI(self):
        self.setGeometry(800, 200, 800, 300)       

        self.ta1 = QTableWidget(self)
        self.ta1.resize(400, 500)
        self.ta1.setColumnCount(3)       

        table_column=["첫번째 열" , "두번째 열" , "Third 열"]
        self.ta1.setHorizontalHeaderLabels(table_column)      
        
        #행 2개 추가
        self.ta1.setRowCount(2)
        
        #추가된 행에 데이터 채워넣음
        self.ta1.setItem(0, 0, QTableWidgetItem("(0,0)"))
        self.ta1.setItem(0, 1, QTableWidgetItem("(0,1)"))
        self.ta1.setItem(1, 0, QTableWidgetItem("(1,0)"))
        self.ta1.setItem(1, 1, QTableWidgetItem("(1,1)"))

        item = self.ta1.item(1,1)
        print("수정전 flag 값 출력: %d " %item.flags().__index__())
        item.setFlags(item.flags() ^  QtCore.Qt.ItemFlag.ItemIsEditable)
        print("Eidtable flag clear 한 후 flag 값 출력: %d " %item.flags().__index__())
        
        
if __name__ == "__main__":
    app = QApplication(sys.argv)
    mywindow = MyWindow()
    mywindow.show()
    app.exec_()
 
>>> 수정전 flag 값 출력: 63
>>> Eidtable flag clear 한 후 flag 값 출력: 61
 
 

 

 

 

 

반응형