113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
"""
|
|
QAbstractTableModel that wraps a list of plain tuples (rows) for display
|
|
in a QTableView. Supports sorting and in-place data refresh.
|
|
"""
|
|
from PyQt6.QtCore import (
|
|
QAbstractTableModel, QModelIndex, Qt, QSortFilterProxyModel
|
|
)
|
|
from PyQt6.QtGui import QColor, QFont
|
|
|
|
|
|
class ResultTableModel(QAbstractTableModel):
|
|
"""Immutable result-set model — replaces data via set_data()."""
|
|
|
|
NULL_COLOR = QColor("#6c7086") # muted grey for NULL
|
|
NUM_ALIGN = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
|
TEXT_ALIGN = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
|
|
|
_NUMERIC_TYPES = (int, float)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._columns: list = []
|
|
self._rows: list = []
|
|
|
|
# ── Public API ────────────────────────────────────────────────────────────
|
|
|
|
def set_data(self, columns: list, rows: list) -> None:
|
|
self.beginResetModel()
|
|
self._columns = list(columns)
|
|
self._rows = [tuple(r) for r in rows]
|
|
self.endResetModel()
|
|
|
|
def clear(self) -> None:
|
|
self.set_data([], [])
|
|
|
|
def get_row(self, row: int) -> tuple:
|
|
return self._rows[row]
|
|
|
|
def column_names(self) -> list:
|
|
return list(self._columns)
|
|
|
|
def export_csv(self, filepath: str, delimiter: str = ",") -> None:
|
|
import csv
|
|
with open(filepath, "w", newline="", encoding="utf-8") as f:
|
|
writer = csv.writer(f, delimiter=delimiter)
|
|
writer.writerow(self._columns)
|
|
writer.writerows(self._rows)
|
|
|
|
def export_json(self, filepath: str) -> None:
|
|
import json
|
|
records = [dict(zip(self._columns, row)) for row in self._rows]
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
json.dump(records, f, indent=2, default=str)
|
|
|
|
def export_sql(self, filepath: str, table_name: str = "table") -> None:
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
cols = ", ".join(self._columns)
|
|
for row in self._rows:
|
|
vals = ", ".join(
|
|
"NULL" if v is None else f"'{str(v).replace(chr(39), chr(39)*2)}'"
|
|
for v in row
|
|
)
|
|
f.write(f"INSERT INTO {table_name} ({cols}) VALUES ({vals});\n")
|
|
|
|
# ── QAbstractTableModel interface ─────────────────────────────────────────
|
|
|
|
def rowCount(self, parent=QModelIndex()) -> int:
|
|
return len(self._rows)
|
|
|
|
def columnCount(self, parent=QModelIndex()) -> int:
|
|
return len(self._columns)
|
|
|
|
def headerData(self, section: int, orientation, role=Qt.ItemDataRole.DisplayRole):
|
|
if role == Qt.ItemDataRole.DisplayRole:
|
|
if orientation == Qt.Orientation.Horizontal:
|
|
return self._columns[section] if section < len(self._columns) else ""
|
|
else:
|
|
return str(section + 1)
|
|
if role == Qt.ItemDataRole.FontRole and orientation == Qt.Orientation.Horizontal:
|
|
f = QFont()
|
|
f.setBold(True)
|
|
return f
|
|
return None
|
|
|
|
def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
|
|
if not index.isValid():
|
|
return None
|
|
row, col = index.row(), index.column()
|
|
if row >= len(self._rows) or col >= len(self._columns):
|
|
return None
|
|
value = self._rows[row][col]
|
|
|
|
if role == Qt.ItemDataRole.DisplayRole:
|
|
if value is None:
|
|
return "NULL"
|
|
return str(value)
|
|
|
|
if role == Qt.ItemDataRole.ForegroundRole and value is None:
|
|
return self.NULL_COLOR
|
|
|
|
if role == Qt.ItemDataRole.TextAlignmentRole:
|
|
if isinstance(value, self._NUMERIC_TYPES):
|
|
return int(self.NUM_ALIGN)
|
|
return int(self.TEXT_ALIGN)
|
|
|
|
if role == Qt.ItemDataRole.UserRole:
|
|
return value # raw Python value
|
|
|
|
return None
|
|
|
|
def flags(self, index: QModelIndex):
|
|
return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
|