Thor's Columns
PyQtでお手軽GUI開発♪―――は可能だったか? 第8回 モデル&ビュー敗北編

PyQtでお手軽GUI開発♪―――は可能だったか? 第8回 モデル&ビュー敗北編

 前回、モデルビューのサンプルをいくつか作りましたが、最後に自力でモデルを作ろうとか言う人の養分になるかもしれないので、最初に作ったQAbstractItemModel継承のサンプルも出しておきましょう。

roleはとても大切だった

 さて、ここでまず問題になっていたのは以下の関数でした。

def data(self, index: Qc.QModelIndex, role=Qt.DisplayRole) -> Any:

 まずこのdataという関数なのですが、最初に画面表示が全く行われなかったのはこいつの書き方が間違っていたせいでした。

 これは表示する値を返すだけの関数なのですが、よく見るとroleというパラメータがあります。これは様々な状況で返すべき値を区別するもののようですが、当面必要なのは表示する文字列だけなので関係ないだろうと、当初はroleを無視して一律にその文字列を返していたわけです。

 ところが何をやっても表示されなくて、webで何とか動くサンプルを見つけ出して(Qt4だったのを5に移植して)確かにそれでは表示されるのに、自分で書いた物では見た感じどこも違っていないように思えるのにやっぱり表示されません。そしてついに一行一行比較していって行き着いたのがこのroleでした。


 そこで初めてroleの説明を見にいって―――のけぞったわけです。要するにこいつに一律に文字列なんかを返したら動きっこないのです(これに気づくのに二日くらいかかったような気が……はあ……)


ModelIndexの謎

 続いてこいつですが、

def index(self, row: int, column: int, parent: Qc.QModelIndex) -> Qc.QModelIndex:

 このindex関数はrow,columnという整数値とparentというModelIndexを引数に取って、そこからそれの指すアイテムのModelIndexを計算して返すという関数です。

 そのためにQAbstractItemModelには以下のようなcreateIndexという簡単にModelIndexを作れる関数も用意されています。

QModelIndex QAbstractItemModel::createIndex(int row, int column, void *ptr = Q_NULLPTR) const

 この関数は見ての通り行と列と実データを引数に取って、そこからQModelIndexクラスのインスタンスを作って返してくれます。

 そしてこのQModelIndexというクラスにはinternalPointerという関数がありますが、これがモデルの実データのポインタを返す関数です。


 ともかくここまで分かればあとはindex関数内で

def index(self, row, column, parent):
    ...
    parentItem = parent.internalPointer()
    return createIndex(row, column, parentItem)

といった感じにしてやれば、data関数内で

def data(self, index, role=Qt.DisplayRole):
    ...
    item = index.internalPointer()
    return item[index.row()][index.column()]

という調子で項目の値が取得できるように思います。


 ―――でもこれではダメなのです! というか、これならば情報は全てindex関数の引数に入っているわけで、わざわざオーバーライドをさせる必要はありません。


ModelIndexの……仕様です

 でいろいろ調べて分かったことはまず、createIndexの引数に取るデータは、アイテムが含まれるテーブルデータへの参照ではなく、アイテムが含まれる行への参照でなければならないということでした。

 すなわちそのデータが例えば二次元のリストだった場合

def index(self, row, column, parent):
    ...
    parentItem = parent.internalPointer()
    return createIndex(row, column, parentItem[row])

といった感じで一行分のデータを渡してやらなければなりませんでした。

 こうして得られたItemIndexのinternalPointerにはその一行分のデータが入っているので、そこから列項目を取得するにはdata関数内で

def data(self, index, role=Qt.DisplayRole):
    ...
    item = index.internalPointer()
    return item[index.column()]

といったように行と列を別々の関数内で処理しなければならなかったのです。


 何か中途半端な気がするんですが―――実際前節のようなやり方で書いても何となく動作したりはします(しかし階層の挙動などが変なんで使い物になりませんが)


 そしてともかくこう書かなければならないからこそ、わざわざindex関数をオーバーライドする必要があったわけです。もしそれがテーブルへの参照でよければQtもそれを単にdataに丸投げすればいいわけですが、テーブル行への参照が必要になると、Qtにはもう分からないのでオーバーライドしてもらう必要があったのです。


 どうしてこうなってるかに関しては―――まあ仕様なら仕方ありませんね。諦めましょうw


親は誰だ?

 さらにこのparent関数も問題でした。

def parent(self, index: Qc.QModelIndex) -> Qc.QModelIndex:

 これはindexで表されるアイテムの親インデックスを返す関数なのですが、実はそもそもQModelIndexクラスにはparentという関数があって、既に自分の親が誰か知っているようなのです。

 だとしたら一体何を教えてやればいいというのでしょうか? 育ての親と実の親が違うのでしょうか?


 そこで最初の頃はこれでいいんじゃないの? と思って、indexのparentをreturnしたりしてたんですが―――しかしそれはまさに論外でした。

 そもそもQAbstractItemModelは実データがどのように親子管理をしているかなど知るよしもありません。従ってQModelIndexのparent関数とは、回り回ってユーザーの定義したparentが参照されていただけだったのです。なので、もちろんこんなことをすれば―――無限再帰でスタックがぶっ飛びますw


 というわけでこのparent関数では元データから直接親インデックスを取得して返さなければなりません。


 ここで最初に作ろうとしていたリストの階層表示なのですが、Pythonのリストの場合、ある項目がどんな子を持っているかは分かりますが、どんな親がいるのかはトップからサーチしないと分からないわけです。

 ところがその場合、リストの項目が全て違った値だったならともかく、異なった場所に同じ値や変数があったらidが同じになる場合があって、そうなるともうそれを区別する手段がありません。例えば同じXというオブジェクトが何カ所かにあったら、上からサーチしていっても、それが元々どこにあったXなのかを特定する手段がありません。

 要するにこのやり方で任意のPythonリストを正しくツリー表示するということは不可能なのでした。


とりあえず動くQAbstractItemModel継承のサンプル

 でも値がみんな違っているような場合なら表示はできるんで―――そんな感じのサンプルが以下のような物です。


# -*- coding: utf-8 -*-
"""PyQt5用:MainWindowテンプレート"""

import sys
from PyQt5 import QtCore as Qc, QtGui as Qg, QtWidgets as Qw
from PyQt5.QtCore import Qt

import absdemoui

#==============================================================================
# QAbstractItemModel継承モデルの作成
#==============================================================================
NO_VALUE = Qc.QModelIndex()

class ListTreeModel(Qc.QAbstractItemModel):
    """任意のリストをQAbstractItemModelに設定する"""

    def __init__(self, initdata):
        super().__init__()
        self.set_data(initdata)

    def set_data(self, initdata):
        """リストで初期化する"""

        self._data = initdata

    def index(self, row, column, parent):
        """新たなindexを作成する"""

        #parentが無効だった場合はトップノードなのでメインデータが
        if not parent.isValid():
            parentItem =self._data
        #無効でなければparent.internalPointerがデータコンテナとなる
        else:
            parentItem = parent.internalPointer()

        if isinstance(parentItem, list):
            return self.createIndex(row, column, parentItem[row])
        else:
            return NO_VALUE         #無効な場合はこの値(Qc.QModelIndex())を返す

    def data(self, index, role=Qt.DisplayRole):
        """指定されたindexの値を取得する

        roleは文字列以外に様々なデータが要求される
        従ってデフォルト処理させたい場合はNoneを返さねばならない!
        """
        item = index.internalPointer()
        if role == Qt.DisplayRole:
            if index.column() == 0:
                return str(item)
            else:
                return 'ディスプレイロール'     #2列目以降は適当な文字列を返しておく
        elif role == Qt.UserRole:
            if index.column() == 0:
                return item
            else:
                return 'ユーザーロール'
        else:
            return None     #ここが問題だったのかああああ!

    def rowCount(self, index):
        """indexで示されるアイテムの行数を取得する"""

        if not index.isValid():
            owner = self._data
        else:
            owner = index.internalPointer()
        if isinstance(owner,list):
            return len(owner)
        else:
            return 1

    def columnCount(self, index):
        """indexで示されるアイテムの列数を取得する"""

        return 2

    def _find_parent(self, item, searchlist=None):
        """指定されたitemの親コンテナ(とその中の行数)を取得する

        item: 調べたいitem
        searchlist: 調べたい親コンテナ
        ※元データが単なるリストで親への参照を持っていないため、頭から探さなければならない
        """
        #searchlistがNoneならトップからの探索
        if searchlist is None:
            searchlist = self._data
        if isinstance(searchlist,list):
            for i in range(len(searchlist)):
                #searchlistにitemがいた場合searchlistが親
                if searchlist[i] is item:
                    return i, searchlist
                #いなければ再帰的に探す
                else:
                    pos, rlist = self._find_parent(item, searchlist[i])
                    if rlist is not None:
                        return pos, rlist
        return None, None

    def parent(self, index):
        """indexの親のQModelIndexを求める"""

        item = index.internalPointer()
        r, parentitem = self._find_parent(item)
        if parentitem is None:
            return NO_VALUE
        elif parentitem == self._data:
            return NO_VALUE
        else:
            return self.createIndex(r, 0 , parentitem)

    def headerData(self, section, orientation, role):
        """ヘッダーを出力する

        ※ここでもroleはちゃんとチェックする
        """
        if (orientation==Qt.Horizontal and role==Qt.DisplayRole):
            if section == 0:
                return 'ListView Test'
            else:
                return 'アイテムの説明'

#==============================================================================
# メインフォーム
# UIはtreeViewだけを貼った単なるMainWindow
#==============================================================================
class MyForm(Qw.QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = absdemoui.Ui_MainWindow()
        self.ui.setupUi(self)

        #元データをセットしてビューに表示させる
        dat = ['1',['2',['3','4'],'5','6'],'7',['8','9'],'10']
        tree = ListTreeModel(dat)
        self.ui.treeView.setModel(tree)

if __name__ == '__main__':
    app = Qw.QApplication(sys.argv)
    wmain = MyForm()
    wmain.show()
    sys.exit(app.exec())

 んで、できたのが以下の奴ですw




 正直これでちゃんと理解できているのか自分でも心許ないのですが―――ま、そういうところはとっとと忘れて、次回はコンテキストメニューとかの細々したネタをお送りします。


2017-04-14