<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>「QAbstractTableModel」タグの記事一覧Python Tech</title>
	<atom:link href="https://tech.nkhn37.net/tag/qabstracttablemodel/feed/" rel="self" type="application/rss+xml" />
	<link>https://tech.nkhn37.net</link>
	<description>Python学習サイト</description>
	<lastBuildDate>Wed, 07 Jan 2026 21:11:25 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://tech.nkhn37.net/wp-content/uploads/2021/01/cropped-lion-normal-clear-1-32x32.png</url>
	<title>「QAbstractTableModel」タグの記事一覧Python Tech</title>
	<link>https://tech.nkhn37.net</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>【PyQt】QTableViewとQAbstractTableModelの使用方法 ~簡易CSVエディタの作成~</title>
		<link>https://tech.nkhn37.net/pyqt-qtableview-qabstracttablemodel-csveditor/</link>
					<comments>https://tech.nkhn37.net/pyqt-qtableview-qabstracttablemodel-csveditor/#respond</comments>
		
		<dc:creator><![CDATA[naoki-hn]]></dc:creator>
		<pubDate>Sat, 18 Feb 2023 20:00:00 +0000</pubDate>
				<category><![CDATA[PyQt]]></category>
		<category><![CDATA[CSV]]></category>
		<category><![CDATA[QAbstractTableModel]]></category>
		<category><![CDATA[QTableView]]></category>
		<guid isPermaLink="false">https://tech.nkhn37.net/?p=7165</guid>

					<description><![CDATA[PythonのGUIツールキットであるPyQtで提供されているQTableViewとQAbstructTableModelの使用方法について紹介します。 QTableViewとQAbstructTableModel Py [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">PythonのGUIツールキットであるPyQtで提供されている<span class="marker"><strong>QTableView</strong></span>と<span class="marker"><strong>QAbstructTableModel</strong></span>の使用方法について紹介します。</p>



<h2 class="wp-block-heading jinr-heading d--bold">QTableViewとQAbstructTableModel</h2>



<p class="wp-block-paragraph">PyQtでは、GUIアプリケーションを開発するときの開発パターンとしてModel-Viewパターンでの開発の枠組みが提供されています。Model-View開発の概要については「<a href="https://tech.nkhn37.net/pyqt-model-view-class-basics/" target="_blank" rel="noreferrer noopener">Model-Viewを使ったアプリケーション開発</a>」でまとめていますので興味があれば参考にしてください。</p>



<p class="wp-block-paragraph">PyQtでは、テーブル用に<span class="marker"><strong>QTableView</strong></span>というビュークラスと<span class="marker"><strong>QAbstructTableModel</strong></span>というモデルの抽象クラスが用意されています。QAbstructTableModelはその名の通り、抽象クラスのため使用するためには自分で各種メソッドの実装が必要になります。</p>



<p class="wp-block-paragraph">本記事では、QTableViewとQAbstructTableModelの使用方法を簡易的なCSVエディタを作成することで整理をしてみたいと思います。</p>



<p class="wp-block-paragraph">※QTableViewとQAbstructTableModelの概要を整理する目的で作成した簡易的なものであるため細部処理の考慮が行き届いていない部分がある点はご容赦ください。</p>



<h2 class="wp-block-heading jinr-heading d--bold">QTableView, QAbstructTableModelを用いた簡易CSVエディタ開発</h2>



<h3 class="wp-block-heading jinr-heading d--bold">簡易CSVエディタの概要</h3>



<p class="wp-block-paragraph">以降で説明する簡易CSVエディタのイメージは以下のような形になります。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img fetchpriority="high" decoding="async" width="1003" height="971" src="https://tech.nkhn37.net/wp-content/uploads/2023/02/image-34.png" alt="QTableView QAbstructTableModel 簡易CSVエディタ" class="wp-image-7193" srcset="https://tech.nkhn37.net/wp-content/uploads/2023/02/image-34.png 1003w, https://tech.nkhn37.net/wp-content/uploads/2023/02/image-34-300x290.png 300w, https://tech.nkhn37.net/wp-content/uploads/2023/02/image-34-768x743.png 768w" sizes="(max-width: 1003px) 100vw, 1003px" /></figure>
</div>


<p class="wp-block-paragraph">CSVファイルを開いて保存ができるようになっており、編集機能として行の追加・削除、列の追加・削除の機能を持っています。また、セルは編集できるようになっていて、セルに対して上下左右に行や列の追加できます（セルが選択されていない時は上下左右の端に行や列が追加されます）</p>



<p class="wp-block-paragraph">以降で、上記画面をQTableViewとQAbstructTableModelを使って実装してみます。</p>



<h3 class="wp-block-heading jinr-heading d--bold">簡易CSVエディタの実装</h3>



<p class="wp-block-paragraph">以降で紹介するプログラムの概要を書いてみると以下のような構成になっています。</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img decoding="async" width="1024" height="681" src="https://tech.nkhn37.net/wp-content/uploads/2023/02/image-36-1024x681.png" alt="QTableView QAbstructTableModel 簡易CSVエディタ" class="wp-image-7199" srcset="https://tech.nkhn37.net/wp-content/uploads/2023/02/image-36-1024x681.png 1024w, https://tech.nkhn37.net/wp-content/uploads/2023/02/image-36-300x200.png 300w, https://tech.nkhn37.net/wp-content/uploads/2023/02/image-36-768x511.png 768w, https://tech.nkhn37.net/wp-content/uploads/2023/02/image-36.png 1454w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</div>


<p class="wp-block-paragraph">MainWindowの中にビュー用のViewクラスとCSVファイルを扱うためのCsvTabModelクラスのインスタンスを持っています。</p>



<p class="wp-block-paragraph">Viewクラスの中にはQTableViewを含む画面構成となっていて、CsvTableModelはQAbstructTableModelを継承して実装します。CsvTableModel内の点線で囲っている部分は、QAbstructTableModelのメソッドでCsvTableModelでオーバーライドして実装します。</p>



<p class="wp-block-paragraph">MainWindowからは[ファイル]メニューや[編集]メニューのアクションに対するメソッドを介してビューやモデルを使用します。</p>



<h4 class="wp-block-heading jinr-heading d--bold">実装例</h4>



<p class="wp-block-paragraph">では、上記概要図で説明したプログラムを実際に以下に示します。詳細は以降で部分ごとに説明していきます。</p>



<p class="wp-block-paragraph">以降のプログラムを実行する際には色々なところにブレークポイントを置いてデバッグで動作を確認してみることをおすすめします。画面でどの操作をすると、どのメソッドに入るかが少しずつ分かってくるかと思います。</p>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">import csv
import sys

from PyQt6 import QtCore as qtc
from PyQt6 import QtGui as qtg
from PyQt6 import QtWidgets as qtw


class CsvTableModel(qtc.QAbstractTableModel):
    """CSVデータ用テーブルモデル"""

    # エラーシグナル
    error = qtc.pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.filepath = None
        self._header = None
        self._data = None

    def csv_open(self, filepath):
        """CSVファイルを読み込みデータをセットする

        Args:
            filepath: CSVファイルパス
        """
        if filepath:
            self.filepath = filepath
            error = ""
            try:
                with open(self.filepath, "r", encoding="utf-8") as f:
                    csv_reader = csv.reader(f)
                    self._header = next(csv_reader)
                    self._data = list(csv_reader)
            except Exception as ex:
                error = f"ファイルを開けませんでした。: {ex}"

            if error:
                self.error.emit(error)
                return False

            return True

    def csv_save(self, filename=None):
        """CSVファイルを保存する"""
        if filename:
            target_file = filename
        else:
            target_file = self.filepath

        if target_file:
            error = ""
            try:
                with open(target_file, "w", newline="", encoding="utf-8") as f:
                    writer = csv.writer(f)
                    writer.writerow(self._header)
                    writer.writerows(self._data)
            except Exception as ex:
                error = f"ファイルを開けませんでした。: {ex}"

            if error:
                self.error.emit(error)
                return False

            # 保存に成功したら現在のファイルパスを更新
            self.filepath = target_file
            return True

    # ===== QAbstractTableModelを使う際に最低限必要なメソッド
    # rowCount, column, data
    def rowCount(self, parent):
        """テーブルの行数を返却する"""
        return len(self._data)

    def columnCount(self, parent):
        """テーブルの列数を返却する"""
        return len(self._header)

    def data(self, index, role):
        """index, roleで指定されるセルのデータを返却する"""
        if role in (
            qtc.Qt.ItemDataRole.DisplayRole,
            qtc.Qt.ItemDataRole.EditRole,
        ):
            return self._data[index.row()][index.column()]

    # ===== データのソート関連メソッド
    # headerData, sort
    def headerData(self, section, orientation, role):
        """ヘッダーデータを返却する"""
        if (
            orientation == qtc.Qt.Orientation.Horizontal
            and role == qtc.Qt.ItemDataRole.DisplayRole
        ):
            return self._header[section]
        else:
            return super().headerData(section, orientation, role)

    def sort(self, column, order):
        # ソートを実行する前に必要
        self.layoutAboutToBeChanged.emit()
        # データをソートする
        self._data.sort(key=lambda x: x[column])
        if order == qtc.Qt.SortOrder.DescendingOrder:
            self._data.reverse()
        # ソート後に必要
        self.layoutChanged.emit()

    # ===== 書き込み対応のためのメソッド
    # flags, setData
    def flags(self, index):
        """フラグの設定"""
        return super().flags(index) | qtc.Qt.ItemFlag.ItemIsEditable

    def setData(self, index, value, role):
        """データを設定する"""
        if index.isValid() and role == qtc.Qt.ItemDataRole.EditRole:
            # valueをindexに該当するセルに設定する
            self._data[index.row()][index.column()] = value
            self.dataChanged.emit(index, index, [role])
            return True
        else:
            return False

    # ===== 行追加／行削除用メソッド
    # insertRows, removeRows
    def insertRows(self, row, count, parent):
        """行追加"""
        self.beginInsertRows(parent or qtc.QModelIndex(), row, row + count - 1)
        new_row = [""] * len(self._header)
        for _ in range(count):
            self._data.insert(row, new_row)
        self.endInsertRows()

    def removeRows(self, row, count, parent):
        """行削除"""
        self.beginRemoveRows(parent or qtc.QModelIndex(), row, row + count - 1)
        for _ in range(count):
            del self._data[row]
        self.endRemoveRows()

    # ===== 列追加／列削除用メソッド
    # insertColumns, removeColumns
    def insertColumns(self, column, count, parent, col_name=""):
        """列追加"""

        if col_name:
            self.beginInsertColumns(
                parent or qtc.QModelIndex(), column, column + count - 1
            )
            new_column = [""] * count
            new_header = [col_name] * count
            self._header[column : column + count - 1] = new_header
            for i, _ in enumerate(self._data):
                self._data[i][column : column + count - 1] = new_column
            self.endInsertColumns()

    def removeColumns(self, column, count, parent):
        """列削除"""
        self.beginRemoveColumns(parent or qtc.QModelIndex(), column, column + count - 1)
        del self._header[column : column + count]
        for i, _ in enumerate(self._data):
            del self._data[i][column : column + count]
        self.endRemoveColumns()


class View(qtw.QWidget):
    """MainView"""

    def __init__(self):
        """Viewコンストラクタ"""
        super().__init__()

        # ===== 画面要素の定義
        # テーブルビューの生成
        self.table_view = qtw.QTableView()
        # ソート可否の設定
        self.table_view.setSortingEnabled(True)

        layout = qtw.QVBoxLayout()
        self.label = qtw.QLabel("", self)
        layout.addWidget(self.label)
        layout.addWidget(self.table_view)
        self.setLayout(layout)

    def show_error_message(self, error):
        """エラーダイアログ表示"""
        qtw.QMessageBox.critical(None, "エラー", error)

    def show_col_input_dialog(self):
        """列名指定ダイアログ"""
        col_name, input_ok = qtw.QInputDialog.getText(self, "列名指定", "新規列名を入力してください。")

        if input_ok:
            return col_name
        else:
            return ""


class MainWindow(qtw.QMainWindow):
    """メインウィンドウ"""

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("CSVエディタ")
        # 画面サイズの設定
        self.resize(640, 360)

        # モデルを生成
        self.model = None
        # Viewを生成
        self.view = View()
        # CentralWidgetへ設定
        self.setCentralWidget(self.view)

        # [ファイル]メニュー
        self.file_menu = self.menuBar().addMenu("ファイル(&amp;F)")
        # [編集]メニュー
        self.edit_menu = self.menuBar().addMenu("編集(&amp;E)")

        # ===== [ファイル]メニューのアクションの追加
        # [ファイルを開く...] アクション
        self.open_action = qtg.QAction(
            "ファイルを開く...",
            self,
            triggered=self.select_file,
            shortcut=qtg.QKeySequence("Ctrl+O"),
        )
        self.file_menu.addAction(self.open_action)

        # [保存] アクション
        self.save_action = qtg.QAction(
            "保存",
            self,
            triggered=self.save_file,
            shortcut=qtg.QKeySequence("Ctrl+S"),
            enabled=False,
        )
        self.file_menu.addAction(self.save_action)

        # [名前を付けて保存...] アクション
        self.save_as_action = qtg.QAction(
            "名前を付けて保存",
            self,
            triggered=self.save_file_as,
            shortcut=qtg.QKeySequence("Ctrl+Shift+S"),
            enabled=False,
        )
        self.file_menu.addAction(self.save_as_action)

        # [終了] アクション
        self.quit_action = self.file_menu.addAction("終了", self.close)

        # ===== [編集]メニューのアクション追加
        # [上に行を追加する] アクション
        self.insert_above_action = qtg.QAction(
            "上に行を追加する",
            self,
            triggered=self.insert_above,
            enabled=False,
        )
        self.edit_menu.addAction(self.insert_above_action)

        # [下に行を追加する] アクション
        self.insert_below_action = qtg.QAction(
            "下に行を追加する",
            self,
            triggered=self.insert_below,
            enabled=False,
        )
        self.edit_menu.addAction(self.insert_below_action)

        # [行を削除する] アクション
        self.remove_rows_action = qtg.QAction(
            "行を削除する",
            self,
            triggered=self.remove_rows,
            enabled=False,
        )
        self.edit_menu.addAction(self.remove_rows_action)

        # [左に列を追加する] アクション
        self.insert_left_action = qtg.QAction(
            "左に列を追加する",
            self,
            triggered=self.insert_left,
            enabled=False,
        )
        self.edit_menu.addAction(self.insert_left_action)

        # [右に列を追加する] アクション
        self.insert_right_action = qtg.QAction(
            "右に列を追加する",
            self,
            triggered=self.insert_right,
            enabled=False,
        )
        self.edit_menu.addAction(self.insert_right_action)

        # [列を削除する] アクション
        self.remove_columns_action = qtg.QAction(
            "列を削除する",
            self,
            triggered=self.remove_columns,
            enabled=False,
        )
        self.edit_menu.addAction(self.remove_columns_action)

        # 画面表示
        self.show()

    def select_file(self):
        """対象ファイルを選択する"""
        filename, _ = qtw.QFileDialog.getOpenFileName(
            self,
            "ファイルを開く",
            qtc.QDir.currentPath(),
            "CSVファイル (*.csv);;すべてのファイル (*)",
            "CSVファイル (*.csv)",
            options=qtw.QFileDialog.Option.DontUseNativeDialog
            | qtw.QFileDialog.Option.DontResolveSymlinks,
        )
        # ファイル読み込み
        if filename:
            if not self.model:
                # モデルが生成されていない場合に生成
                self.model = CsvTableModel()
                self.model.error.connect(self.view.show_error_message)

            # ファイルをオープンして、モデルをセットする
            if self.model.csv_open(filename):
                self.view.table_view.setModel(self.model)
                # アクションの有効化
                self.save_action.setEnabled(True)
                self.save_as_action.setEnabled(True)
                self.insert_above_action.setEnabled(True)
                self.insert_below_action.setEnabled(True)
                self.remove_rows_action.setEnabled(True)
                self.insert_left_action.setEnabled(True)
                self.insert_right_action.setEnabled(True)
                self.remove_columns_action.setEnabled(True)
                # ファイルパス更新
                self.view.label.setText(f"{filename}")

    def save_file_as(self):
        """名前を付けて保存する"""
        # ファイルダイアログを表示
        filename, _ = qtw.QFileDialog.getSaveFileName(
            self,
            "ファイルを保存する",
            qtc.QDir.currentPath(),
            "CSVファイル (*.csv);;すべてのファイル (*)",
            "CSVファイル (*.csv)",
            qtw.QFileDialog.Option.DontUseNativeDialog
            | qtw.QFileDialog.Option.DontResolveSymlinks,
        )
        # ファイル書き込み
        if filename and self.model:
            if self.model.csv_save(filename):
                # ファイルパス更新
                self.view.label.setText(f"{filename}")

    def save_file(self):
        """上書き保存する"""
        if self.model:
            self.model.csv_save()

    def insert_above(self):
        """選択行の上に行を追加する"""
        selected = self.view.table_view.selectedIndexes()
        row = selected[0].row() if selected else 0
        self.model.insertRows(row, 1, None)

    def insert_below(self):
        """選択行の下に行を追加する"""
        selected = self.view.table_view.selectedIndexes()
        row = selected[-1].row() if selected else self.model.rowCount(None)
        self.model.insertRows(row + 1, 1, None)

    def remove_rows(self):
        """選択行を削除する"""
        selected = self.view.table_view.selectedIndexes()
        num_rows = len(set(i.row() for i in selected))
        if selected:
            self.model.removeRows(selected[0].row(), num_rows, None)

    def insert_left(self):
        """選択列の左に列を追加する"""
        col_name = self.view.show_col_input_dialog()
        if col_name:
            selected = self.view.table_view.selectedIndexes()
            column = selected[0].column() if selected else 0
            self.model.insertColumns(column, 1, None, col_name)

    def insert_right(self):
        """選択列の右に列を追加する"""
        col_name = self.view.show_col_input_dialog()
        if col_name:
            selected = self.view.table_view.selectedIndexes()
            column = selected[-1].column() if selected else self.model.columnCount(None)
            self.model.insertColumns(column + 1, 1, None, col_name)

    def remove_columns(self):
        """選択列を削除する"""
        selected = self.view.table_view.selectedIndexes()
        num_columns = len(set(i.column() for i in selected))
        if selected:
            self.model.removeColumns(selected[0].column(), num_columns, None)


def main():
    """メイン関数"""
    app = qtw.QApplication(sys.argv)
    mv = MainWindow()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
</pre>



<p class="wp-block-paragraph">以降で、上記プログラムについて部分ごとに内容を説明していきます。なお、プログラムの全体構成は「<a rel="noreferrer noopener" href="https://tech.nkhn37.net/pyqt-qmainwindow-program-template/" target="_blank">QMainWindowを継承した画面開発のテンプレート</a>」をベースにしています。</p>



<h4 class="wp-block-heading jinr-heading d--bold">CsvTableModelクラスの実装</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">class CsvTableModel(qtc.QAbstractTableModel):
    """CSVデータ用テーブルモデル"""

    # エラーシグナル
    error = qtc.pyqtSignal(str)</pre>



<p class="wp-block-paragraph">今回のプログラムの中心となるモデルは上記のようにQAbstructTableModelを継承して実装します。ファイル操作に失敗した場合用にエラー用シグナルを用意しています。</p>



<h5 class="wp-block-heading jinr-heading d--bold"><strong>コンストラクタ __init__</strong>()</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    def __init__(self):
        super().__init__()
        self.filepath = None
        self._header = None
        self._data = None</pre>



<p class="wp-block-paragraph">コンストラクタでは、csvファイルのファイルパス(filepath)、ヘッダー(_header)、データ(_data)を用意しています。</p>



<h5 class="wp-block-heading jinr-heading d--bold">CSV操作メソッド：csv_open, csv_save</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    def csv_open(self, filepath):
        """CSVファイルを読み込みデータをセットする

        Args:
            filepath: CSVファイルパス
        """
        if filepath:
            self.filepath = filepath
            error = ""
            try:
                with open(self.filepath, "r", encoding="utf-8") as f:
                    csv_reader = csv.reader(f)
                    self._header = next(csv_reader)
                    self._data = list(csv_reader)
            except Exception as ex:
                error = f"ファイルを開けませんでした。: {ex}"

            if error:
                self.error.emit(error)
                return False

            return True

    def csv_save(self, filename=None):
        """CSVファイルを保存する"""
        if filename:
            target_file = filename
        else:
            target_file = self.filepath

        if target_file:
            error = ""
            try:
                with open(target_file, "w", newline="", encoding="utf-8") as f:
                    writer = csv.writer(f)
                    writer.writerow(self._header)
                    writer.writerows(self._data)
            except Exception as ex:
                error = f"ファイルを開けませんでした。: {ex}"

            if error:
                self.error.emit(error)
                return False

            # 保存に成功したら現在のファイルパスを更新
            self.filepath = target_file
            return True</pre>



<p class="wp-block-paragraph">上記のメソッドはcsvモジュールを使ったcsvファイルを操作するための簡単なメソッドなので細部説明は省略します。csvモジュールの基本は「<a href="https://tech.nkhn37.net/python-csv/" target="_blank" rel="noreferrer noopener">CSVファイルの入出力</a>」でもまとめていますので興味があれば参考にしてください。</p>



<p class="wp-block-paragraph">エラー時にはerror文字列を生成して、エラーシグナルのerror.emit()でシグナルを発生させます。これは後ほどのViewクラスで定義するエラーダイアログのスロットメソッドに接続します。</p>



<h5 class="wp-block-heading jinr-heading d--bold">最低限実装が必要なメソッド：rowCount, columnCount, data</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    # ===== QAbstractTableModelを使う際に最低限必要なメソッド
    # rowCount, column, data
    def rowCount(self, parent):
        """テーブルの行数を返却する"""
        return len(self._data)

    def columnCount(self, parent):
        """テーブルの列数を返却する"""
        return len(self._header)

    def data(self, index, role):
        """index, roleで指定されるセルのデータを返却する"""
        if role in (
            qtc.Qt.ItemDataRole.DisplayRole,
            qtc.Qt.ItemDataRole.EditRole,
        ):
            return self._data[index.row()][index.column()]</pre>



<p class="wp-block-paragraph">QAbstructTableModelを使う上では最低限実装が必要なメソッドとして、<span class="marker"><strong>rowCount</strong></span>、<span class="marker"><strong>columnCount</strong></span>、<span class="marker"><strong>data</strong></span>といったメソッドがあります。その名の通りrowCountやcolumnCountは行数や列数を返却するメソッドで、_dataや_headerを使って長さを返却しています。</p>



<p class="wp-block-paragraph">また、dataメソッドは、indexやroleで指定されるセルのデータを返却するメソッドです。indexは、QModelIndexクラスのインスタンスでテーブル選択個所の位置を扱うことができます。index.row()で行番号、index.column()で列番号が取得できます。</p>



<p class="wp-block-paragraph">また、roleはQtCoreで定義されているQt.ItemDataRoleのいずれに該当するかを記載しています。DisplayRoleは表示、EditRoleは編集を意味するものです。これらのRoleに一致する場合はindexで指定される位置のデータの値を返却します。</p>



<h5 class="wp-block-heading jinr-heading d--bold">ソートに必要なメソッド：headerData, sort</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    # ===== データのソート関連メソッド
    # headerData, sort
    def headerData(self, section, orientation, role):
        """ヘッダーデータを返却する"""
        if (
            orientation == qtc.Qt.Orientation.Horizontal
            and role == qtc.Qt.ItemDataRole.DisplayRole
        ):
            return self._header[section]
        else:
            return super().headerData(section, orientation, role)

    def sort(self, column, order):
        # ソートを実行する前に必要
        self.layoutAboutToBeChanged.emit()
        # データをソートする
        self._data.sort(key=lambda x: x[column])
        if order == qtc.Qt.SortOrder.DescendingOrder:
            self._data.reverse()
        # ソート後に必要
        self.layoutChanged.emit()</pre>



<p class="wp-block-paragraph">テーブルを操作する際に列名でソートしたくなることがありますが、その場合には<span class="marker"><strong>headerData</strong></span>と<span class="marker"><strong>sort</strong></span>メソッドの実装が必要です。</p>



<p class="wp-block-paragraph">headerDataメソッドでは、sectionで指定される列番号のヘッダーを返却します。</p>



<p class="wp-block-paragraph">sortメソッドでは、_dataをソートするのですがポイントとしてソートを実行する前に<span class="marker"><strong>layoutAboutToBeChanged.emit</strong></span>を、ソートを完了した後に<span class="marker"><strong>layoutChanged.emit</strong></span>のシグナルを出すことが必要です。</p>



<h5 class="wp-block-heading jinr-heading d--bold">セルの値の変更に必要なメソッド：flags, setData</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    # ===== 書き込み対応のためのメソッド
    # flags, setData
    def flags(self, index):
        """フラグの設定"""
        return super().flags(index) | qtc.Qt.ItemFlag.ItemIsEditable

    def setData(self, index, value, role):
        """データを設定する"""
        if index.isValid() and role == qtc.Qt.ItemDataRole.EditRole:
            # valueをindexに該当するセルに設定する
            self._data[index.row()][index.column()] = value
            self.dataChanged.emit(index, index, [role])
            return True
        else:
            return False</pre>



<p class="wp-block-paragraph">各セルの値を変更するためには上記の<span class="marker"><strong>flags</strong></span>と<span class="marker"><strong>setData</strong></span>といったメソッドの実装が必要です。各セルの値を読み取り専用で編集できないようにしたい場合は、コメントアウトしてもらえば読み取り専用のテーブルにできます。</p>



<p class="wp-block-paragraph">flagsメソッドでは、qtc.Qt.ItemFlag.ItemEditableをフラグに|で追加することで編集可能であることを指定しているような形になっています。</p>



<p class="wp-block-paragraph">また、setDataメソッドの方は指定された位置(index)の位置に値(value)を設定するメソッドだと思ってください。「index.isValid()」の結果でインデックスが妥当であり、かつ「EditRole」の場合にデータを指定インデックスの位置に設定しています。</p>



<p class="wp-block-paragraph">また、データ変更後には、データが変更されたことが分かるように<span class="marker"><strong>dataChanged.emit</strong></span>でデータが変更されたというシグナルを出すことが必要です。</p>



<h5 class="wp-block-heading jinr-heading d--bold">行追加・削除メソッド：insertRows, removeRows</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    # ===== 行追加／行削除用メソッド
    # insertRows, removeRows
    def insertRows(self, row, count, parent):
        """行追加"""
        self.beginInsertRows(parent or qtc.QModelIndex(), row, row + count - 1)
        new_row = [""] * len(self._header)
        for _ in range(count):
            self._data.insert(row, new_row)
        self.endInsertRows()

    def removeRows(self, row, count, parent):
        """行削除"""
        self.beginRemoveRows(parent or qtc.QModelIndex(), row, row + count - 1)
        for _ in range(count):
            del self._data[row]
        self.endRemoveRows()</pre>



<p class="wp-block-paragraph">上記は行を追加・削除しているメソッドで、選択されている行(row)、行数(count)を引数として受け取り_dataを操作して空行を追加したり、指定行をdelで削除しています。</p>



<p class="wp-block-paragraph">ここでポイントとなるのは行追加の場合は、<span class="marker"><strong>beginInsertRows</strong></span>と<span class="marker"><strong>endInsertRows</strong></span>で、行削除の場合は、<span class="marker"><strong>beginRemoveRows</strong></span>と<span class="marker"><strong>endRemoveRows</strong></span>で処理を囲うということです。これはPyQt側が必要な変更のために処理をするために必要なものということでお決まりとして理解してもらえばいいのかなと思います。</p>



<h5 class="wp-block-heading jinr-heading d--bold">列追加・削除メソッド：insertColumns, removeColumns</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    # ===== 列追加／列削除用メソッド
    # insertColumns, removeColumns
    def insertColumns(self, column, count, parent, col_name=""):
        """列追加"""

        if col_name:
            self.beginInsertColumns(
                parent or qtc.QModelIndex(), column, column + count - 1
            )
            new_column = [""] * count
            new_header = [col_name] * count
            self._header[column : column + count - 1] = new_header
            for i, _ in enumerate(self._data):
                self._data[i][column : column + count - 1] = new_column
            self.endInsertColumns()

    def removeColumns(self, column, count, parent):
        """列削除"""
        self.beginRemoveColumns(parent or qtc.QModelIndex(), column, column + count - 1)
        del self._header[column : column + count]
        for i, _ in enumerate(self._data):
            del self._data[i][column : column + count]
        self.endRemoveColumns()</pre>



<p class="wp-block-paragraph">上記は列を追加・削除しているメソッドで、選択されている列(column)、行数(count)を引数として受け取り_headerや_dataに対して追加や削除を行います。また、追加の際には列名に指定された文字列を列名として列を作成しています。</p>



<p class="wp-block-paragraph">ここでポイントとなるのは行の追加・削除と同じで、追加の際は<span class="marker"><strong>beginInsertColumns</strong></span>と<span class="marker"><strong>endInsertColumns</strong></span>で、列削除の場合には<span class="marker"><strong>beginRemoveColumns</strong></span>と<span class="marker"><strong>endRemoveColumns</strong></span>で囲うことになります。</p>



<h4 class="wp-block-heading jinr-heading d--bold">Viewクラスの実装</h4>



<p class="wp-block-paragraph">Viewクラスは、今回の簡易CSVエディタの画面構成を定義しているものです。</p>



<h5 class="wp-block-heading jinr-heading d--bold">コンストラクタ：__init__()</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">class View(qtw.QWidget):
    """MainView"""

    def __init__(self):
        """Viewコンストラクタ"""
        super().__init__()

        # ===== 画面要素の定義
        # テーブルビューの生成
        self.table_view = qtw.QTableView()
        # ソート可否の設定
        self.table_view.setSortingEnabled(True)

        layout = qtw.QVBoxLayout()
        self.label = qtw.QLabel("", self)
        layout.addWidget(self.label)
        layout.addWidget(self.table_view)
        self.setLayout(layout)</pre>



<p class="wp-block-paragraph">コンストラクタでテーブルビューであるQTableViewをtable_viewとして生成しています。また、テーブルがソート可能なようにsetSortingEnableでTrueを指定しています。</p>



<p class="wp-block-paragraph">あとは、QVBoxLayoutでファイルパスを表示するための「label」とテーブルビューである「table_view」を垂直にレイアウトしています。</p>



<h5 class="wp-block-heading jinr-heading d--bold">エラーダイアログ</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    def show_error_message(self, error):
        """エラーダイアログ表示"""
        qtw.QMessageBox.critical(None, "エラー", error)</pre>



<p class="wp-block-paragraph">上記は、error文字列を受け取ってエラーダイアログを出す簡単なメソッドです。モデル側でCSVファイルが開けなかった際のシグナルと接続してエラーを表示します。</p>



<h5 class="wp-block-heading jinr-heading d--bold">列名指定ダイアログ</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    def show_col_input_dialog(self):
        """列名指定ダイアログ"""
        col_name, input_ok = qtw.QInputDialog.getText(self, "列名指定", "新規列名を入力してください。")

        if input_ok:
            return col_name
        else:
            return ""</pre>



<p class="wp-block-paragraph">上記は、列追加が呼び出されたときに列名を指定するようなダイアログです。以下のような画面で列名を入力できるようにします。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="202" height="128" src="https://tech.nkhn37.net/wp-content/uploads/2023/02/image-37.png" alt="QTableView QAbstructTableModel 簡易CSVエディタ 列名指定ダイアログ" class="wp-image-7260"/></figure>
</div>


<h4 class="wp-block-heading jinr-heading d--bold">MainWindowクラスの実装</h4>



<p class="wp-block-paragraph">最後にMainWindowクラスの説明をします。MainWindowクラスは、ビューとモデルを含んだようなものになっています。</p>



<h5 class="wp-block-heading jinr-heading d--bold">コンストラクタ：__init__()</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">class MainWindow(qtw.QMainWindow):
    """メインウィンドウ"""

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("CSVエディタ")
        # 画面サイズの設定
        self.resize(640, 360)

        # モデルを生成
        self.model = None
        # Viewを生成
        self.view = View()
        # CentralWidgetへ設定
        self.setCentralWidget(self.view)

        # ================ メニュー追加等についての説明は省略

        # 画面表示
        self.show()</pre>



<p class="wp-block-paragraph">コンストラクタでは、モデル(model)とビュー(view)を生成し、ビューの方は上記で作ったViewクラスをインスタンス化し、setCentralWidgetで中心ウィジェットに設定しています。なお、モデルの方はファイルを開く操作の時に生成することとして初期の生成時にはNoneとしています。</p>



<p class="wp-block-paragraph">メニューの追加等のコードについては「<a rel="noreferrer noopener" href="https://tech.nkhn37.net/pyqt-qmainwindow-program-template/" target="_blank">QMainWindowを継承した画面開発のテンプレート</a>」のページでも説明しているため今回詳細は省略します。なお、アプリケーション初期表示時点でファイルを選択されていない時には保存や編集等はできる必要はないため、enabled=Falseとして以下のように選択できないようにしています。以降でファイルを選択(select_file)する際にメニューのアクションを有効化します。</p>



<div class="wp-block-columns js--scr-animation is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:66.66%"><div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="269" height="168" src="https://tech.nkhn37.net/wp-content/uploads/2023/02/image-39.png" alt="QTableView QAbstructTableModel 簡易CSVエディタ メニューの無効化" class="wp-image-7269"/></figure>
</div></div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:33.33%">
<figure class="wp-block-image size-full"><img decoding="async" width="220" height="200" src="https://tech.nkhn37.net/wp-content/uploads/2023/02/image-40.png" alt="QTableView QAbstructTableModel 簡易CSVエディタ メニューの無効化" class="wp-image-7270"/></figure>
</div>
</div>



<h5 class="wp-block-heading jinr-heading d--bold">ファイル選択メソッド：select_file</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    def select_file(self):
        """対象ファイルを選択する"""
        filename, _ = qtw.QFileDialog.getOpenFileName(
            self,
            "ファイルを開く",
            qtc.QDir.currentPath(),
            "CSVファイル (*.csv);;すべてのファイル (*)",
            "CSVファイル (*.csv)",
            options=qtw.QFileDialog.Option.DontUseNativeDialog
            | qtw.QFileDialog.Option.DontResolveSymlinks,
        )
        # ファイル読み込み
        if filename:
            if not self.model:
                # モデルが生成されていない場合に生成
                self.model = CsvTableModel()
                self.model.error.connect(self.view.show_error_message)

            # ファイルをオープンして、モデルをセットする
            if self.model.csv_open(filename):
                self.view.table_view.setModel(self.model)
                # アクションの有効化
                self.save_action.setEnabled(True)
                self.save_as_action.setEnabled(True)
                self.insert_above_action.setEnabled(True)
                self.insert_below_action.setEnabled(True)
                self.remove_rows_action.setEnabled(True)
                self.insert_left_action.setEnabled(True)
                self.insert_right_action.setEnabled(True)
                self.remove_columns_action.setEnabled(True)
                # ファイルパス更新
                self.view.label.setText(f"{filename}")</pre>



<p class="wp-block-paragraph">select_fileメソッドはファイルダイアログを開いて指定したファイル名を読み込みます。</p>



<p class="wp-block-paragraph">ファイル名が読み込まれた場合でモデルが生成されていない場合にはモデルを生成し、エラーシグナルとエラーメッセージのスロットを接続します。その後、ファイルをオープンしてQTableViewのsetModelでモデルをセットすることで使えるようにします。</p>



<p class="wp-block-paragraph">また、保存や編集アクションなどファイルを開いた際に有効化するべきアクションをsetEnabledにTrueを設定することで有効化しています。</p>



<h5 class="wp-block-heading jinr-heading d--bold">名前を付けて保存：save_file_as　上書き保存：save_file</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    def save_file_as(self):
        """名前を付けて保存する"""
        # ファイルダイアログを表示
        filename, _ = qtw.QFileDialog.getSaveFileName(
            self,
            "ファイルを保存する",
            qtc.QDir.currentPath(),
            "CSVファイル (*.csv);;すべてのファイル (*)",
            "CSVファイル (*.csv)",
            qtw.QFileDialog.Option.DontUseNativeDialog
            | qtw.QFileDialog.Option.DontResolveSymlinks,
        )
        # ファイル書き込み
        if filename and self.model:
            if self.model.csv_save(filename):
                # ファイルパス更新
                self.view.label.setText(f"{filename}")

    def save_file(self):
        """上書き保存する"""
        if self.model:
            self.model.csv_save()</pre>



<p class="wp-block-paragraph">上記メソッドは、ファイルを保存するためのメソッドです。名前を付けて保存の際にはファイルダイアログを開いて選択してから、選択ファイルパスを引数に指定してモデルのcsv_saveを呼び出します。名前を付けて保存の場合は、別ファイルになるので画面のファイルパス表示を更新しています。</p>



<p class="wp-block-paragraph">また、上書き保存の際には既にモデルに設定されているファイルパスに対して保存を実行するため、引数なしでモデルのcsv_saveを呼び出します。</p>



<h5 class="wp-block-heading jinr-heading d--bold">編集関連メソッド：insert_above, insert_below, remove_rows, insert_left, insert_right, remove_columns</h5>



<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">   def insert_above(self):
        """選択行の上に行を追加する"""
        selected = self.view.table_view.selectedIndexes()
        row = selected[0].row() if selected else 0
        self.model.insertRows(row, 1, None)

    def insert_below(self):
        """選択行の下に行を追加する"""
        selected = self.view.table_view.selectedIndexes()
        row = selected[-1].row() if selected else self.model.rowCount(None)
        self.model.insertRows(row + 1, 1, None)

    def remove_rows(self):
        """選択行を削除する"""
        selected = self.view.table_view.selectedIndexes()
        num_rows = len(set(i.row() for i in selected))
        if selected:
            self.model.removeRows(selected[0].row(), num_rows, None)

    def insert_left(self):
        """選択列の左に列を追加する"""
        col_name = self.view.show_col_input_dialog()
        if col_name:
            selected = self.view.table_view.selectedIndexes()
            column = selected[0].column() if selected else 0
            self.model.insertColumns(column, 1, None, col_name)

    def insert_right(self):
        """選択列の右に列を追加する"""
        col_name = self.view.show_col_input_dialog()
        if col_name:
            selected = self.view.table_view.selectedIndexes()
            column = selected[-1].column() if selected else self.model.columnCount(None)
            self.model.insertColumns(column + 1, 1, None, col_name)

    def remove_columns(self):
        """選択列を削除する"""
        selected = self.view.table_view.selectedIndexes()
        num_columns = len(set(i.column() for i in selected))
        if selected:
            self.model.removeColumns(selected[0].column(), num_columns, None)</pre>



<p class="wp-block-paragraph">上記メソッド群は、編集メソッドから呼び出される行や列の追加削除メソッドです。</p>



<p class="wp-block-paragraph">基本的にはtable_viewの<span class="marker"><strong>selectedIndexes</strong></span>で指定されたインデックス情報を取得してきて、モデルクラスに実装した各種の行、列の追加・削除メソッドを呼び出しています。列追加の際には、新規列名が分からないのでViewクラスの列名指定ダイアログで列名を取得してから列追加メソッドを呼び出します。</p>



<p class="wp-block-paragraph">以上が、QTableViewとQAbstructTableModelを使った簡易CSVエディタの概要説明でした。</p>



<h2 class="wp-block-heading jinr-heading d--bold">まとめ</h2>



<p class="wp-block-paragraph">PythonのGUIツールキットであるPyQtで提供されている<span class="marker"><strong>QTableView</strong></span>と<span class="marker"><strong>QAbstructTableModel</strong></span>の使用方法について紹介しました。QTableViewとQAbstructTableModelの使用方法を簡易的なCSVエディタを作成することで整理、紹介しています。</p>



<p class="wp-block-paragraph">QAbstructTableModelの使用では、いくつかオーバーライドして実装するべきメソッドがありますので実装時の参考にしていただければと思います。</p>



<p class="wp-block-paragraph">なお、プログラムを実装する際には色々なところにブレークポイントを置いてデバッグで動作を確認してみることをおすすめします。画面でどの操作をすると、どのメソッドに入るかが少しずつ分かってくるかと思います。</p>



<p class="wp-block-paragraph">※QTableViewとQAbstructTableModelの概要を整理する目的で作成した簡易的なものであるため細部処理の考慮が行き届いていない部分がある点はご容赦ください。</p>



<section class="wp-block-jinr-blocks-simplebox b--jinr-block-container"><div class="b--jinr-block b--jinr-box d--heading-box8  "><div class="a--simple-box-title d--bold">ソースコード</div><div class="c--simple-box-inner">
<p class="wp-block-paragraph">上記で紹介しているソースコードについては <a href="https://github.com/nkhn37/python-tech-sample-source/tree/main/python-libraries/pyqt/model-view-sample/csv-editor" target="_blank" rel="noreferrer noopener">GitHub</a> にて公開しています。参考にしていただければと思います。</p>
</div></div></section>


<section class="b--jinr-block b--jinr-blogcard d--blogcard-hover-up d--blogcard-style1 d--blogcard-mysite t--round "><div class="a--blogcard-label ef">あわせて読みたい</div><a class="o--blogcard-link t--round" href="https://tech.nkhn37.net/python-tech-summary-page/"><div class="c--blogcard-image"><img decoding="async" class="a--blogcard-img-src" width="128" height="72" src="https://tech.nkhn37.net/wp-content/uploads/2024/08/Python-Tech-Pythonプログラミングガイド_new1-640x360.jpg" alt="【Python Tech】プログラミングガイド" /></div><div class="a--blogcard-title d--bold">【Python Tech】プログラミングガイド</div></a></section>


<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://tech.nkhn37.net/pyqt-qtableview-qabstracttablemodel-csveditor/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Disk: Enhanced  を使用したページ キャッシュ

Served from: tech.nkhn37.net @ 2026-06-14 16:23:59 by W3 Total Cache
-->