처음에는 QTableWidget을 사용하여 표를 구성하였지만 조금 알아보니 QTableView를 사용하는 것이 성능면에서
더 낫다는 듯 해서 교체작업을 진행했다.

 

1. 테이블 뷰 생성

from PySide6.QtWidgets import QTabWidget, QWidget, QGridLayout, QTableView

from controller import actions
from models.table_models import DayCalTableModel, DayCalOthersTableModel, DayCalResultTableModel


# 일일 정산서 계산서 위젯
class DayCal(QWidget):
    # 생성자
    def __init__(self):
        super().__init__()
        self.input_table = QTableView()
        self.other_table = QTableView()
        self.result_table = QTableView()
        self.init_ui()

	''' 생략 '''

 화주별 데이터를 입력받고 일부 결과를 표시하기 위한 input_table, 화주와 관계없는 별도의 데이터를 입력하기
위한 other_table, 입력데이터를 기반으로 결과를 나타낼 result_table 로 총 세개의 QTableView를 생성하였다.

 

 

2. input_table 테이블 모델 작성

import operator

from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *


class DayCalTableModel(QAbstractTableModel):
    def __init__(self, parent, horizontal_header, data, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.setParent(parent)
        self.table_data = data
        self.horizontal_header = horizontal_header
        self.vertical_header = ['강동총금액', '강동운임', '강동하차비', '강동수수료 4%', '공제후 금액', '중매수수료 5%', '화주운임', '화주하차비', '상장수수료 4%', '강동선지급금', '공제합계', '선지급금포함 공제합계']
        self.row_count = len(self.vertical_header)
        self.column_count = len(self.horizontal_header)

    def rowCount(self, parent):
        return self.row_count

    def columnCount(self, parent):
        return self.column_count

    def data(self, index, role):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self.table_data[index.column()][index.row()]
                return str(value)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return self.horizontal_header[section]
            else:
                return self.vertical_header[section]
        return None

    def sort(self, col, order):
        self.emit(SIGNAL("layoutAboutToBeChanged()"))
        self.table_data = sorted(self.table_data, key=operator.itemgetter(col))
        if order == Qt.DescendingOrder:
            self.table_data.reverse()
        self.emit(SIGNAL("layoutChanged()"))

    def flags(self, index):
        if index.row() in [3, 4, 10, 11]:
            return Qt.ItemIsSelectable | Qt.ItemIsEnabled
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self.table_data[index.column()][index.row()] = value
            return True
        return False

    def owner_added(self, name):
        self.beginInsertColumns(QModelIndex(), self.column_count, self.column_count)
        self.horizontal_header.append(name)
        self.table_data.append([0]*12)
        self.column_count += 1
        self.endInsertColumns()

    def owner_removed(self, name):
        idx = self.horizontal_header.index(name)
        self.beginRemoveColumns(QModelIndex(), idx, idx)
        self.table_data.pop(idx)
        self.horizontal_header.pop(idx)
        self.column_count -= 1
        self.endRemoveColumns()

    def owner_modified(self, org, chg):
        idx = self.horizontal_header.index(org)
        self.horizontal_header[idx] = chg
        
    ''' 생략 '''

여기서는 QAbstractTableModel 을 상속한 테이블 모델을 작성하였다. (이 외에도 다른 여러가지 방법들이 있는 듯 했다.)

 

  • __init__(self, parent, horizontal_header, data, *args)
    • parent 위젯과 헤더, 그리고 표시할 데이터를 입력받는 초기화 메소드
    • 해당 모델의 parent를 입력받은 parent 위젯으로 설정
    • 해당 모델의 table_data 를 입력받은 데이터로 설정한다.  
    • horizontal_header를 입력받은 헤더 데이터로 설정한다. 이는 화주 명단과 같다.
    • vertical_header를 설정한다. 이 항목들은 변경되지 않기 때문에 하드코딩으로 설정해주었다.
    • row_count, column_count(행과 열의 길이) 값을 각각 vertical, horizontal 헤더의 길이로 설정한다.

 

  • rowCount(self, parent) / columnCount(self, parent)
    • QAbstractModel에 정의된 rowCount, columnCount 메소드를 오버라이딩
    • 테이블의 열 수와 행 수를 반환한다.

 

  • data(self, index, role)
    • QAbstractModel에 정의된 data 메소드를 오버라이딩

    • role은 이 메소드를 어떤 상태(역할)에서 호출하는지를 나타내며 index는 참조하려는 데이터의
      위치(행과 열)를 나타낸다. role 과 index에 따라 적절히 데이터를 반환하도록 구현한다.

    • 여기서는 Qt.DisplayRole(표시), Qt.EditRole(수정) 상태에서 호출할 경우 index에 해당하는 데이터를
      반환하도록 구현하였다. 만약 role == Qt.EditRole을 조건에서 뺄 경우 수정상태에 들어간 셀에서는
      데이터가 표시되지 않는다.

 

  • headerData(self, section, orientation, role)
    • QAbstractModel에 정의된 headerData 메소드를 오버라이딩

    • orientation은 방향(행방향 / 열방향)을 나타내며 section은 인덱스를 나타낸다.

    • horizontal, vertical 의 헤더를 반환하기 위해 사용한다.
    • orientation 이 horizontal인지 vertical인지에 따라 각각 해당하는 헤더값을 반환하도록 구현하였다.

 

  • flags(self, index)
    • QAbstractModel에 정의된 flags 메소드를 오버라이딩

    • index 에 해당하는 셀에 대해 flag를 설정하기 위해 사용한다

    • 플래그를 통해 특정 셀의 편집가능 여부를 수정하는 등의 작업이 가능하다.

    • 사용자가 값을 입력하는 셀이 아닌 부분을 편집 불가능하게 설정하였다.

 

  • setData(self, index, value, role)
    • QAbstractModel에 정의된 setData 메소드를 오버라이딩

    • 셀의 값 변경을 반영하기 위해 구현해야한다.

    • edit role 일 경우에 수정사항이 반영되도록 구현하였다.

    • 반영되었을 경우 True, 아닐 경우 False를 반환한다.

 

  • owner_added(self, name) / owner_removed(self, name) / owner_modified(self, org, chg)
    • 화주의 추가, 삭제, 이름변경을 반영하기 위해 직접 구현한 메소드이다.

    • 화주가 추가되고 그에따라 화주별 데이터도 추가되어 데이터베이스에 반영되지만
      데이터베이스에서 데이터를 읽어오는 것은 프로그램 실행시에만 수행하는 작업이며
      반영된 데이터를 굳이 데이터베이스에서 다시 읽어오는것도 비효율적이기에
      모델의 메소드를 호출하여 즉각 반영하도록 하였다. 

    • added 와 removed 의 경우 테이블 자체가 변경되는 작업이기에 (열의 추가/삭제)
      beginInsertColumns, beginRemoveColumns 등 테이블의 변경작업이 시작됨을 알리는
      메소드를 호출한 뒤 작업을 수행하고 마지막에 endInsertColumns, endRemoveColumns 등
      테이블의 변경작업이 끝났음을 알리는 메소드를 호출해줘야 변경사항이 정상적으로 반영된다.

    • modified 의 경우 단순히 값을 수정하는 것 뿐이기에 header 리스트를 수정해주는 것만으로
      간단하게 반영된다.

 

 

3. other_table 테이블 모델 작성

import operator

from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *

''' 생략 '''

class DayCalOthersTableModel(QAbstractTableModel):
    def __init__(self, parent, data, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.setParent(parent)
        self.table_data = [data]
        self.vertical_header = ['경매 사무실 입금', '가라경매 강동 입금', '직접 지출', '우리 경매', '강동 사입']
        self.row_count = len(self.vertical_header)
        self.column_count = 1

    def rowCount(self, parent):
        return self.row_count

    def columnCount(self, parent):
        return self.column_count

    def data(self, index, role):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self.table_data[index.column()][index.row()]
                return str(value)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
        if role == Qt.DisplayRole:
            if orientation == Qt.Vertical:
                return self.vertical_header[section]
        return None

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self.table_data[index.column()][index.row()] = value
            return True
        return False

''' 생략 '''
  • other_table 의 테이블 모델도 기본적으로 input_table의 그것과 거의 동일하게 구현

  • horizontal header가 필요하지 않기 때문에 vertical header만을 설정

  • 모두 사용자가 입력하는 데이터로만 이루어졌기에 특정 셀의 수정 불가등의 설정을 하지 않음

 

 

4. result_table 테이블 모델 작성

import operator

from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *

''' 생략 '''

class DayCalResultTableModel(QAbstractTableModel):
    def __init__(self, parent, result_data, *args):
        QAbstractTableModel.__init__(self, parent, *args)
        self.setParent(parent)
        self.horizontal_header = ['계']
        self.vertical_header = ['강동총금액 합계', '강동운임 합계', '강동하차비 합계', '강동수수료 4% 합계', '공제후금액 합계',
                                '중매수수료 5% 합계', '화주운임 합계', '화주하차비 합계', '상장수수료 4% 합계', '경매확인',
                                '경매 차액', '중개수수료 5%', '경매 차익']
        self.row_count = len(self.vertical_header)
        self.column_count = 1

        self.table_data = [result_data]

    def rowCount(self, parent):
        return self.row_count

    def columnCount(self, parent):
        return self.column_count

    def data(self, index, role):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self.table_data[index.column()][index.row()]
                return str(value)

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
        if role == Qt.DisplayRole:
            if orientation == Qt.Vertical:
                return self.vertical_header[section]
            else:
                return self.horizontal_header[section]
        return None

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled
  • 마찬가지로 input_table과 거의 동일하게 구현

  • 해당 테이블은 모든 셀을 수정 불가능하도록 설정

 

 

5. 테이블 뷰에 모델 세팅

from PySide6.QtWidgets import QTabWidget, QWidget, QGridLayout, QTableView

from controller import actions
from models.table_models import DayCalTableModel, DayCalOthersTableModel, DayCalResultTableModel


# 일일 정산서 계산서 위젯
class DayCal(QWidget):
    # 생성자
    def __init__(self):
        super().__init__()
        self.input_table = QTableView()
        self.other_table = QTableView()
        self.result_table = QTableView()
        self.init_ui()

    # ui 초기화
    def init_ui(self):
        # 화주별 데이터 입력테이블
        self.data_model = DayCalTableModel(self, actions.get_daycal_owner_list(), actions.get_daycal_owner_values())
        self.input_table.setModel(self.data_model)

        # 기타 데이터 입력 테이블
        self.other_data_model = DayCalOthersTableModel(self, actions.get_daycal_other_values())
        self.other_table.setModel(self.other_data_model)

        # 결과 테이블
        self.result_data_model = DayCalResultTableModel(self, actions.get_daycal_result())
        self.result_table.setModel(self.result_data_model)

        # 그리드 레이아웃
        grid = QGridLayout()

        # 테이블위젯 추가
        grid.addWidget(self.input_table, 0, 0)
        grid.addWidget(self.other_table, 1, 0)
        grid.addWidget(self.result_table, 0, 1, 2, 1)
        grid.setRowStretch(0, 5)
        grid.setColumnStretch(0, 5)

        # 레이아웃 세팅
        self.setLayout(grid)
        
    # 화주 추가 반영
    def owner_added(self, added_user):
        self.data_model.owner_added(added_user)

    # 화주 삭제 반영
    def owner_removed(self, removed_name):
        self.data_model.owner_removed(removed_name)

    # 화주 이름 변경 반영
    def owner_modified(self, org_name, chg_name):
        self.data_model.owner_modified(org_name, chg_name)
        
''' 생략 '''
  • 작성한 모델의 인스턴스를 생성(이 때, DB에서 데이터를 읽어와서 인자로 넘겨준다.)

  • 테이블 뷰에 각각 해당하는 모델의 인스턴스를 세팅

  • 레이아웃에 추가한 뒤 위젯에 레이아웃을 세팅한다.

  • DayCal 위젯은 owner_added, owner_removed, owner_modified 메소드를 각각 구현하여
    화주 상태변경을 인식하고 각 테이블의 데이터모델의 메소드를 호출하여 이를 반영한다.

 

 

6. DB 작업을 위한 함수 작성

# 화주 명단 가져오기
def get_daycal_owner_list():
    return [q.name for q in session.query(DayCalOwner).order_by(DayCalOwner.id)]


# 화주별 데이터 가져오기
def get_daycal_owner_values():
    values = []
    today = date.today()
    for owner_id in session.query(DayCalOwner.id).order_by(DayCalOwner.id):
        id = owner_id[0]
        value = session.query(DayCalOwnerValues).filter(and_(DayCalOwnerValues.owner_id == id, DayCalOwnerValues.date == today)).first()
        if not value:
            value = DayCalOwnerValues(today, id)
            session.add(value)
            session.commit()
        values.append(value.to_list())
    return values


# 기타 데이터 가져오기
def get_daycal_other_values():
    today = date.today()
    value = session.query(DayCalOtherValues).filter(DayCalOtherValues.date == today).first()
    if not value:
        value = DayCalOtherValues(today)
        session.add(value)
        session.commit()
    return value.to_list()


# 결과 데이터 가져오기
def get_daycal_result():
    today = date.today()
    value = session.query(DayCalResult).filter(DayCalResult.date == today).first()
    if not value:
        value = DayCalResult(today)
        session.add(value)
        session.commit()
    return value.to_list()
  • 데이터 모델의 인스턴스 생성에 필요한 데이터를 DB에서 가져오기 위한 함수 구현

  • get_daycal_owner_list
    • 화주 이름 명단을 반환

    • 화주 id를 기준으로 오름차순 정렬

 

  • get_daycal_owner_values
    • 화주별 오늘의 데이터를 리스트 형태로 반환

    • 화주 id를 기준으로 오름차순 정렬

    • 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
      데이터베이스에 추가하고 해당 데이터를 반환한다.

 

  • get_daycal_other_values
    • 오늘의 기타 데이터를 리스트 형태로 반환

    • 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
      데이터베이스에 추가하고 해당 데이터를 반환한다.

 

  • get_daycal_result
    • 오늘의 결과 데이터를 리스트 형태로 반환

    • 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
      데이터베이스에 추가하고 해당 데이터를 반환한다.

 

 

7. to_list 메소드 추가

# 화주별 일일정산 데이터 모델
class DayCalOwnerValues(Base):
    __tablename__ = 'daycal_owner_values'
    date = Column(Date, primary_key=True)
    owner_id = Column(Integer, primary_key=True)
    kd_total = Column(Integer, nullable=False)
    kd_fare = Column(Integer, nullable=False)
    kd_drop = Column(Integer, nullable=False)
    kd_fee4 = Column(Integer, nullable=False)
    after_deduction = Column(Integer, nullable=False)
    match_fee5 = Column(Integer, nullable=False)
    owner_fare = Column(Integer, nullable=False)
    owner_drop = Column(Integer, nullable=False)
    listing_fee4 = Column(Integer, nullable=False)
    kd_pre = Column(Integer, nullable=False)
    deduction_total = Column(Integer, nullable=False)
    total_include_pre = Column(Integer, nullable=False)

    def __init__(self, date, owner_id):
        self.date = date
        self.owner_id = owner_id
        self.kd_total = 0
        self.kd_fare = 0
        self.kd_drop = 0
        self.kd_fee4 = 0
        self.after_deduction = 0
        self.match_fee5 = 0
        self.owner_fare = 0
        self.owner_drop = 0
        self.listing_fee4 = 0
        self.kd_pre = 0
        self.deduction_total = 0
        self.total_include_pre = 0

    def to_list(self):
        return [self.kd_total, self.kd_fare, self.kd_drop, self.kd_fee4, self.after_deduction,
                self.match_fee5, self.owner_fare, self.owner_drop, self.listing_fee4, self.kd_pre,
                self.deduction_total, self.total_include_pre]


# 일일정산서에 기타 데이터 모델
class DayCalOtherValues(Base):
    __tablename__ = 'daycal_other_values'
    date = Column(Date, primary_key=True)
    office_deposit = Column(Integer)
    kd_deposit = Column(Integer)
    direct_exp = Column(Integer)
    our_auc = Column(Integer)
    kd_buy = Column(Integer)

    def __init__(self, date):
        self.date = date
        self.office_deposit = 0
        self.kd_deposit = 0
        self.direct_exp = 0
        self.our_auc = 0
        self.kd_buy = 0

    def to_list(self):
        return [self.office_deposit, self.kd_deposit, self.direct_exp, self.our_auc, self.kd_buy]


# 일일정산서 결과 모델
class DayCalResult(Base):
    __tablename__ = 'daycal_result'
    date = Column(Date, primary_key=True)
    kd_total = Column(Integer, nullable=False)
    kd_fare = Column(Integer, nullable=False)
    kd_drop = Column(Integer, nullable=False)
    kd_fee4 = Column(Integer, nullable=False)
    after_deduction = Column(Integer, nullable=False)
    match_fee5 = Column(Integer, nullable=False)
    owner_fare = Column(Integer, nullable=False)
    owner_drop = Column(Integer, nullable=False)
    listing_fee4 = Column(Integer, nullable=False)
    auc_check = Column(Integer, nullable=False)
    auc_diff = Column(Integer, nullable=False)
    match_fee5_final = Column(Integer, nullable=False)
    auc_profit = Column(Integer, nullable=False)

    def __init__(self, date):
        self.date = date
        self.kd_total = 0
        self.kd_fare = 0
        self.kd_drop = 0
        self.kd_fee4 = 0
        self.after_deduction = 0
        self.match_fee5 = 0
        self.owner_fare = 0
        self.owner_drop = 0
        self.listing_fee4 = 0
        self.auc_check = 0
        self.auc_diff = 0
        self.match_fee5_final = 0
        self.auc_profit = 0

    def to_list(self):
        return [self.kd_total, self.kd_fare, self.kd_drop, self.kd_fee4, self.after_deduction,
                self.match_fee5, self.owner_fare, self.owner_drop, self.listing_fee4, self.auc_check,
                self.auc_diff, self.match_fee5_final, self.auc_profit]
  • DB에서 모델단위로 읽어온 데이터를 보다 쉽게 리스트 형태로 전달하기 위해 to_list 메소드를 추가

  • 단순히 테이블에 표현하기 위해 필요한 column 값들을 리스트형태로 반환하도록 구현

 

 

이제 날짜가 바뀌면 자동으로 그날의 디폴트 데이터를 DB에 삽입하고 테이블에 표시해준다. 

또한 QTableWidget을 사용했을 때 보다 좀더 유연하게 테이블을 사용할 수 있게 되었다.

+ Recent posts