PyQt5 QListViewのサンプル

Share on:

PyQt5でリストを表示するときに使えるQListViewを使ってみました。

環境

  • Python 3.7.4
  • PyQt 5.9.2

作成した内容

  • Qt Designerでmainwindow.uiを作成してmainwindow.pyに変換
  • モデルの作成
  • デリゲートの作成
  • モデル作成時にオーバーライドが必須なメソッドの作成
  • モデルにアイテムを追加削除するメソッドの作成
  • 上記を行うボタンとコンテキストメニューの作成

モデルクラスのメソッドにあるaddItems(self, items)とremoveItems(self, rows)以外はオーバーライドが(たぶん)必須です。例えばcolumnCount()をコメントアウトすると実行時に下記のように怒られます。

1NotImplementedError: QAbstractItemModel.columnCount() is abstract and must be overridden

モデルにアイテムを追加する

1def addItems(self, items):
2    self.beginInsertRows(QtCore.QModelIndex(), len(self.items), len(self.items) + len(items) - 1)
3    self.items.extend(items)
4    self.endInsertRows()

追加したいアイテムのリストを渡すとモデルに追加するというメソッドを作成しました。単純にリストにextendで追加する処理に加えて、追加したというのをbeginInsertRowsとendInsertRowsで知らせてあげる必要があります。 beginInsertRows では追加する最初の位置と最後の位置を指定してあげます。今回はアイテムの最後に追加するという処理にしています。

モデルのアイテムを削除する

 1def removeItems(self, rows):
 2    sec = [ [rows[0], rows[0]+1] ]
 3    for row in rows[1:]:
 4        if sec[-1][1] == row:
 5            sec[-1][1] = sec[-1][1] + 1
 6            continue
 7        sec.append([row, row + 1])
 8    
 9    for s in sec[::-1]:
10        self.beginRemoveRows(QtCore.QModelIndex(), s[0], s[1])
11        del self.items[s[0]:s[1]]
12        self.endRemoveRows()

削除したい行のリストを渡すとモデルからアイテムを削除するメソッドを作成しました。削除の時も追加と同じように、delでリストから削除してbeginRemoveRowsとendRemoveRowsで削除を知らせてあげます。最初のforの部分は、例えば[1, 2, 3, 7, 8, 9, 10]を渡すと[1, 3]と[7, 10]に置き換えるという処理をしていて、削除回数を減らすために入れています。ちなみにsec[::-1]としているのはリストの後ろから削除しないと削除箇所が合わなくなるからです。

1def removeItems(self, rows):
2    for row in rows[::-1]:
3        self.beginRemoveRows(QtCore.QModelIndex(), row, row)
4        del self.items[row]
5        self.endRemoveRows()

上記のようにしても良かったのですが、削除したい行数が増えると処理に時間がかかってしまたので前述のようにしました。と言っても、10000行を削除するときに前述の場合は0.002秒で後述の場合は0.579秒でしたので、コードがわかりやすい後述の通りでもよいかと思います。

QListViewにコンテキストメニューを追加

1self.ui.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
2self.ui.listView.customContextMenuRequested.connect(self.contextMenu)

モデルへのアイテム追加、削除をGUIで操作できるようにするためにコンテキストメニューを使用しました。自分で作成したメニューを表示させるためにsetContextMenuPolicyでカスタムコンテキストメニューを使用できるように設定して、customContextMenuRequested.connectでコンテキストメニューを表示するメソッドに接続します。

今回作成したモデルとデリゲートはQTableViewでもほぼそのまま使えると思います。QTreeViewでは親子関係が増えるので多少工夫が必要になると思います。そのあたりもためてみようと思います。

ソースコード

main.py

  1# -*- coding: utf-8 -*-
  2import sys
  3from mainwindow import Ui_MainWindow
  4from PyQt5 import QtWidgets, QtCore
  5
  6class Model(QtCore.QAbstractItemModel):
  7    def __init__(self, parent=None):
  8        super(Model, self).__init__(parent)
  9        self.items = []
 10        
 11    def addItems(self, items):
 12        self.beginInsertRows(QtCore.QModelIndex(), len(self.items), len(self.items) + len(items) - 1)
 13        self.items.extend(items)
 14        self.endInsertRows()
 15        
 16    def columnCount(self, parent):
 17        return 1
 18
 19    def data(self, index, role=QtCore.Qt.DisplayRole):
 20        if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
 21            return self.items[index.row()]
 22        
 23    def flags(self, index):
 24        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
 25        
 26    def headerData(self, i, orientation, role):
 27        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
 28            return i
 29        if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
 30            return i
 31    
 32    def index(self, row, column=0, parent=QtCore.QModelIndex()):
 33        return self.createIndex(row, column, parent)
 34    
 35    def parent(self, index):
 36        return QtCore.QModelIndex()
 37    
 38    def removeItems(self, rows):
 39        sec = [ [rows[0], rows[0]+1] ]
 40        for row in rows[1:]:
 41            if sec[-1][1] == row:
 42                sec[-1][1] = sec[-1][1] + 1
 43                continue
 44            sec.append([row, row + 1])
 45        
 46        for s in sec[::-1]:
 47            self.beginRemoveRows(QtCore.QModelIndex(), s[0], s[1])
 48            del self.items[s[0]:s[1]]
 49            self.endRemoveRows()
 50    
 51    def rowCount(self, parent=QtCore.QModelIndex()):
 52        return len(self.items)
 53    
 54    def setData(self, index, value, role=QtCore.Qt.EditRole):
 55        if role == QtCore.Qt.EditRole:
 56            self.items[index.row()] = value
 57            return True
 58        return False
 59
 60class Delegate(QtWidgets.QStyledItemDelegate):
 61    def __init__(self, parent=None, setModelDataEvent=None):
 62        super(Delegate, self).__init__(parent)
 63        self.setModelDataEvent = setModelDataEvent
 64
 65    def createEditor(self, parent, option, index):
 66        return QtWidgets.QLineEdit(parent)
 67
 68    def setEditorData(self, editor, index):
 69        value = index.model().data(index, QtCore.Qt.DisplayRole)
 70        editor.setText(str(value))
 71
 72    def setModelData(self, editor, model, index):
 73        model.setData(index, editor.text())
 74        if not self.setModelDataEvent is None:
 75            self.setModelDataEvent()
 76
 77class MainWindow(QtWidgets.QMainWindow):
 78    def __init__(self, app):
 79        super().__init__()
 80        
 81        self.ui = Ui_MainWindow()
 82        self.ui.setupUi(self)
 83        
 84        self.model = Model(self)
 85        self.ui.listView.setModel(self.model)
 86        self.ui.listView.setItemDelegate(Delegate())
 87        self.ui.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
 88        self.ui.listView.customContextMenuRequested.connect(self.contextMenu)
 89        self.ui.listView.setAlternatingRowColors(True)
 90        
 91        self.ui.pushButton.clicked.connect(self.addItem)
 92        self.ui.pushButton_2.clicked.connect(self.delItem)
 93        
 94    def contextMenu(self, point):
 95        self.menu = QtWidgets.QMenu(self)
 96        self.menu.addAction('追加', self.addItem)
 97        self.menu.addAction('削除', self.delItem)
 98        self.menu.exec_( self.ui.listView.mapToGlobal(point) )
 99        
100    def addItem(self):
101        self.model.addItems([str(self.model.rowCount())])
102        
103    def delItem(self):
104        if len(self.ui.listView.selectedIndexes()) == 0:
105            return
106        rows = [index.row() for index in self.ui.listView.selectedIndexes()]
107        self.model.removeItems(rows)
108        
109def main():
110    app = QtWidgets.QApplication(sys.argv)
111    window = MainWindow(app)
112    window.show()
113    app.exec_()
114
115if __name__ == '__main__':
116    main()

mainwindow.py

 1# -*- coding: utf-8 -*-
 2from PyQt5 import QtCore, QtGui, QtWidgets
 3
 4class Ui_MainWindow(object):
 5    def setupUi(self, MainWindow):
 6        MainWindow.setObjectName("MainWindow")
 7        MainWindow.setEnabled(True)
 8        MainWindow.resize(300, 400)
 9        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
10        sizePolicy.setHorizontalStretch(0)
11        sizePolicy.setVerticalStretch(0)
12        sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
13        MainWindow.setSizePolicy(sizePolicy)
14        MainWindow.setMinimumSize(QtCore.QSize(300, 400))
15        MainWindow.setMaximumSize(QtCore.QSize(300, 400))
16        self.centralwidget = QtWidgets.QWidget(MainWindow)
17        self.centralwidget.setObjectName("centralwidget")
18        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
19        self.verticalLayout.setObjectName("verticalLayout")
20        self.horizontalLayout = QtWidgets.QHBoxLayout()
21        self.horizontalLayout.setObjectName("horizontalLayout")
22        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
23        font = QtGui.QFont()
24        font.setPointSize(11)
25        self.pushButton.setFont(font)
26        self.pushButton.setObjectName("pushButton")
27        self.horizontalLayout.addWidget(self.pushButton)
28        self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget)
29        font = QtGui.QFont()
30        font.setPointSize(11)
31        self.pushButton_2.setFont(font)
32        self.pushButton_2.setObjectName("pushButton_2")
33        self.horizontalLayout.addWidget(self.pushButton_2)
34        self.verticalLayout.addLayout(self.horizontalLayout)
35        self.listView = QtWidgets.QListView(self.centralwidget)
36        font = QtGui.QFont()
37        font.setPointSize(11)
38        self.listView.setFont(font)
39        self.listView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
40        self.listView.setObjectName("listView")
41        self.verticalLayout.addWidget(self.listView)
42        MainWindow.setCentralWidget(self.centralwidget)
43        self.menubar = QtWidgets.QMenuBar(MainWindow)
44        self.menubar.setGeometry(QtCore.QRect(0, 0, 300, 21))
45        self.menubar.setObjectName("menubar")
46        MainWindow.setMenuBar(self.menubar)
47        self.statusbar = QtWidgets.QStatusBar(MainWindow)
48        self.statusbar.setObjectName("statusbar")
49        MainWindow.setStatusBar(self.statusbar)
50
51        self.retranslateUi(MainWindow)
52        QtCore.QMetaObject.connectSlotsByName(MainWindow)
53
54    def retranslateUi(self, MainWindow):
55        _translate = QtCore.QCoreApplication.translate
56        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
57        self.pushButton.setText(_translate("MainWindow", "Add"))
58        self.pushButton_2.setText(_translate("MainWindow", "Delete"))

mainwindow.ui

 1<?xml version="1.0" encoding="UTF-8"?>
 2<ui version="4.0">
 3 <class>MainWindow</class>
 4 <widget class="QMainWindow" name="MainWindow">
 5  <property name="enabled">
 6   <bool>true</bool>
 7  </property>
 8  <property name="geometry">
 9   <rect>
10    <x>0</x>
11    <y>0</y>
12    <width>300</width>
13    <height>400</height>
14   </rect>
15  </property>
16  <property name="sizePolicy">
17   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
18    <horstretch>0</horstretch>
19    <verstretch>0</verstretch>
20   </sizepolicy>
21  </property>
22  <property name="minimumSize">
23   <size>
24    <width>300</width>
25    <height>400</height>
26   </size>
27  </property>
28  <property name="maximumSize">
29   <size>
30    <width>300</width>
31    <height>400</height>
32   </size>
33  </property>
34  <property name="windowTitle">
35   <string>MainWindow</string>
36  </property>
37  <widget class="QWidget" name="centralwidget">
38   <layout class="QVBoxLayout" name="verticalLayout">
39    <item>
40     <layout class="QHBoxLayout" name="horizontalLayout">
41      <item>
42       <widget class="QPushButton" name="pushButton">
43        <property name="font">
44         <font>
45          <pointsize>11</pointsize>
46         </font>
47        </property>
48        <property name="text">
49         <string>Add</string>
50        </property>
51       </widget>
52      </item>
53      <item>
54       <widget class="QPushButton" name="pushButton_2">
55        <property name="font">
56         <font>
57          <pointsize>11</pointsize>
58         </font>
59        </property>
60        <property name="text">
61         <string>Delete</string>
62        </property>
63       </widget>
64      </item>
65     </layout>
66    </item>
67    <item>
68     <widget class="QListView" name="listView">
69      <property name="font">
70       <font>
71        <pointsize>11</pointsize>
72       </font>
73      </property>
74      <property name="selectionMode">
75       <enum>QAbstractItemView::ExtendedSelection</enum>
76      </property>
77     </widget>
78    </item>
79   </layout>
80  </widget>
81  <widget class="QMenuBar" name="menubar">
82   <property name="geometry">
83    <rect>
84     <x>0</x>
85     <y>0</y>
86     <width>300</width>
87     <height>21</height>
88    </rect>
89   </property>
90  </widget>
91  <widget class="QStatusBar" name="statusbar"/>
92 </widget>
93 <resources/>
94 <connections/>
95</ui>

関連記事