Qtで画像を表示できるリストを作成する


### はじめに

Qtを使用したことがある人なら一度はQListWidgetを使ったことがあると思います。
ですが、その見た目を変更したいと思った方も多いのではないでしょうか？

この記事では細かい解説は省いて、できるだけ簡潔にリストをカスタマイズしてみます。


1. QAbstractItemModel
2. QListView
3. QStyledItemDelegate


まずは上記の3つのClassを使用して、シンプルなリストを作成します。


#### 環境
* Python 3.10
* PySide2
* QtPy



---
### シンプルなリストの作成
#### Model
```python
class CustomItemModel(QtCore.QAbstractItemModel):
    def __init__(self, parent=None, data=[]):
        super(CustomItemModel, self).__init__(parent)
        self.__items = data

    def index(self, row, column, parent):
        return self.createIndex(row, column, self.__items[row])

    def parent(self, index):
        return QtCore.QModelIndex()

    def rowCount(self, parent):
        return len(self.__items)

    def columnCount(self, parent):
        return 1

    def data(self, index, role=QtCore.Qt.DisplayRole):
        item = index.internalPointer()
        if role == QtCore.Qt.DisplayRole:
            return item.get('name')
        else:
            return None
```
item = index.internalPointer()　は
self.createIndex()の第三引数 (self.__items[row]) の値が取得できます



---

#### View,Delegate

```python
class CustomListView(QtWidgets.QListView):
    def __init__(self, parent=None):
        super(CustomListView, self).__init__(parent)
```
```python
class CustomItemDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent=None):
        super(CustomItemDelegate, self).__init__(parent)

    def paint(self, painter, option, index):
        painter.save()
        if option.state & QtWidgets.QStyle.State_Selected:
            painter.fillRect(option.rect, option.palette.highlight())
            painter.setPen(option.palette.color(QtGui.QPalette.HighlightedText))

        elif option.state & QtWidgets.QStyle.State_MouseOver:
            painter.fillRect(option.rect, option.palette.midlight())

        name = index.data(QtCore.Qt.DisplayRole)
        painter.drawText(option.rect, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, name)
        painter.restore()
```
painter.save(),painter.restore()について
setPen,setBrush等で変更した設定が残った状態になるため、save,restoreでpainterの初期化しています。



```python
data = [{'name': 'hoge'}, {'name': 'fuga'}]

model = CustomItemModel(data=data)
delegate = CustomItemDelegate()
view = CustomListView()
view.setModel(model)
view.setItemDelegate(delegate)

layout = QtWidgets.QHBoxLayout()
layout.addWidget(view)
ui = QtWidgets.QWidget()
ui.setLayout(layout)
ui.show()
```





![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558544/e62ef3b2-00ac-4313-a101-70f197f93dbe.png)



ここまでで最低限ListWidgetの見た目を再現できました。
上記を踏まえてデータの追加、見た目を変更していきます。

---

### 見た目のカスタマイズ

#### Model


表示する画像をモデルに追加します。

```python
data = [{'name': 'hoge', 'img':'./img.png'}, {'name': 'fuga'}]
```

```python: CustomItemModel
    def data(self, index, role=QtCore.Qt.DisplayRole):
        item = index.internalPointer()
        if role == QtCore.Qt.DisplayRole:
            return item.get('name')
        elif role == QtCore.Qt.UserRole:
            if img:= item.get('img'):
                if os.path.exists(img):
                    return QtGui.QPixmap(img)
            return img
        else:
            return None
```


---

#### View

```python: CustomListView
    def __init__(self, parent=None):
        super(CustomListView, self).__init__(parent)
        self.setFlow(QtWidgets.QListView.Flow.LeftToRight)
        self.setViewMode(QtWidgets.QListView.IconMode)
        self.setResizeMode(QtWidgets.QListView.Adjust)

```
* setFlow : リストの流れを横に変更
* setViewMode : IconModeに変更することでアイテムのサイズに応じて並べる
* setResizeMode : Adjustでviewのサイズが変更されたとき、レイアウトを自動調整

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558544/99107b9b-b6db-ae0a-0b24-78ef5338e138.png)



---
#### Delegate
##### 1.サイズの指定
以下の関数を追加します。
```python: CustomItemDelegate
    def sizeHint(self, option, index):
        return QtCore.QSize(200,150)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558544/7155d55d-fc27-da22-c835-656159b5782c.png)



##### 2.レイアウトの調整


```python: CustomItemDelegate
    def setRectPadding(self, rect , padding=0):
        if not padding:
            return rect
        margin = QtCore.QMargins() + padding
        rect = rect.marginsRemoved(margin / 2)
        frame_rect = self.__rect.marginsRemoved(margin)
        rect = frame_rect.intersected(rect)
        return rect


    def splitVRect(self, rect, y):
        rect1 = rect.adjusted(0, 0, 0, y - rect.height())
        rect2 = rect.adjusted(0, y, 0, 0)
        return rect1, rect2


    def paint(self, painter, option, index):
        painter.save()
        self.__rect = option.rect

        if option.state & QtWidgets.QStyle.State_Selected:
            painter.fillRect(option.rect, option.palette.highlight())
            painter.setPen(option.palette.color(QtGui.QPalette.HighlightedText))

        elif option.state & QtWidgets.QStyle.State_MouseOver:
            painter.fillRect(option.rect, option.palette.midlight())

        name = index.data(QtCore.Qt.DisplayRole)
        img  = index.data(QtCore.Qt.UserRole)

        txt_rect, img_rect = self.splitVRect(option.rect, 24)
        txt_rect = self.setRectPadding(txt_rect, 4)
        img_rect = self.setRectPadding(img_rect, 8)

        painter.drawText(txt_rect, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, name)

        if img:
            img = img.scaled(img_rect.size(), QtCore.Qt.KeepAspectRatio)
            src_rect = QtCore.QRect((img.width()-img_rect.width())/2,
                                    (img.height()-img_rect.height())/2,
                                    img_rect.width(),
                                    img_rect.height())
            painter.drawPixmap(img_rect, img, src_rect)
        else:
            painter.fillRect(img_rect, option.palette.mid())
            painter.setPen(option.palette.color(QtGui.QPalette.HighlightedText))
            painter.drawText(img_rect, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignCenter, 'NoImage')

        painter.restore()
```

* 2つの関数でテキスト,画像用の領域の指定
* imgがある場合、サイズを調整して画像用の領域内で中心に配置

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3558544/536a3870-4816-c21f-7ed6-131412fad71a.png)


### まとめ
今回はListViewで画像を表示できるようになりました。
ListWidget比較して、複雑なデータでも必要な情報に応じて見た目をカスタマイズできるので、よりユーザーにとってわかりやすいUIが作成できるかと思います。

次回はこのリストを便利に使用するために、データの操作などもう少し具体的な機能の紹介を予定しています。