PyQt5 QListViewのサンプル
2020/01/25 categories:PyQt5| tags:Python|PyQt5|
PyQt5でリストを表示するときに使えるQListViewを使ってみました。
環境
- Python 3.7.4
- PyQt 5.9.2
作成した内容
- Qt Designerでmainwindow.uiを作成してmainwindow.pyに変換
- モデルの作成
- デリゲートの作成
- モデル作成時にオーバーライドが必須なメソッドの作成
- モデルにアイテムを追加削除するメソッドの作成
- 上記を行うボタンとコンテキストメニューの作成
モデルクラスのメソッドにあるaddItems(self, items)とremoveItems(self, rows)以外はオーバーライドが(たぶん)必須です。例えばcolumnCount()をコメントアウトすると実行時に下記のように怒られます。
NotImplementedError: QAbstractItemModel.columnCount() is abstract and must be overridden
モデルにアイテムを追加する
def addItems(self, items):
self.beginInsertRows(QtCore.QModelIndex(), len(self.items), len(self.items) + len(items) - 1)
self.items.extend(items)
self.endInsertRows()
追加したいアイテムのリストを渡すとモデルに追加するというメソッドを作成しました。単純にリストにextendで追加する処理に加えて、追加したというのをbeginInsertRowsとendInsertRowsで知らせてあげる必要があります。 beginInsertRows では追加する最初の位置と最後の位置を指定してあげます。今回はアイテムの最後に追加するという処理にしています。
モデルのアイテムを削除する
def removeItems(self, rows):
sec = [ [rows[0], rows[0]+1] ]
for row in rows[1:]:
if sec[-1][1] == row:
sec[-1][1] = sec[-1][1] + 1
continue
sec.append([row, row + 1])
for s in sec[::-1]:
self.beginRemoveRows(QtCore.QModelIndex(), s[0], s[1])
del self.items[s[0]:s[1]]
self.endRemoveRows()
削除したい行のリストを渡すとモデルからアイテムを削除するメソッドを作成しました。削除の時も追加と同じように、delでリストから削除してbeginRemoveRowsとendRemoveRowsで削除を知らせてあげます。最初のforの部分は、例えば[1, 2, 3, 7, 8, 9, 10]を渡すと[1, 3]と[7, 10]に置き換えるという処理をしていて、削除回数を減らすために入れています。ちなみにsec[::-1]としているのはリストの後ろから削除しないと削除箇所が合わなくなるからです。
def removeItems(self, rows):
for row in rows[::-1]:
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
del self.items[row]
self.endRemoveRows()
上記のようにしても良かったのですが、削除したい行数が増えると処理に時間がかかってしまたので前述のようにしました。と言っても、10000行を削除するときに前述の場合は0.002秒で後述の場合は0.579秒でしたので、コードがわかりやすい後述の通りでもよいかと思います。
QListViewにコンテキストメニューを追加
self.ui.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.listView.customContextMenuRequested.connect(self.contextMenu)
モデルへのアイテム追加、削除をGUIで操作できるようにするためにコンテキストメニューを使用しました。自分で作成したメニューを表示させるためにsetContextMenuPolicyでカスタムコンテキストメニューを使用できるように設定して、customContextMenuRequested.connectでコンテキストメニューを表示するメソッドに接続します。
今回作成したモデルとデリゲートはQTableViewでもほぼそのまま使えると思います。QTreeViewでは親子関係が増えるので多少工夫が必要になると思います。そのあたりもためてみようと思います。
ソースコード
main.py
## -*- coding: utf-8 -*-
import sys
from mainwindow import Ui_MainWindow
from PyQt5 import QtWidgets, QtCore
class Model(QtCore.QAbstractItemModel):
def __init__(self, parent=None):
super(Model, self).__init__(parent)
self.items = []
def addItems(self, items):
self.beginInsertRows(QtCore.QModelIndex(), len(self.items), len(self.items) + len(items) - 1)
self.items.extend(items)
self.endInsertRows()
def columnCount(self, parent):
return 1
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
return self.items[index.row()]
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, i, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return i
if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
return i
def index(self, row, column=0, parent=QtCore.QModelIndex()):
return self.createIndex(row, column, parent)
def parent(self, index):
return QtCore.QModelIndex()
def removeItems(self, rows):
sec = [ [rows[0], rows[0]+1] ]
for row in rows[1:]:
if sec[-1][1] == row:
sec[-1][1] = sec[-1][1] + 1
continue
sec.append([row, row + 1])
for s in sec[::-1]:
self.beginRemoveRows(QtCore.QModelIndex(), s[0], s[1])
del self.items[s[0]:s[1]]
self.endRemoveRows()
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
self.items[index.row()] = value
return True
return False
class Delegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None, setModelDataEvent=None):
super(Delegate, self).__init__(parent)
self.setModelDataEvent = setModelDataEvent
def createEditor(self, parent, option, index):
return QtWidgets.QLineEdit(parent)
def setEditorData(self, editor, index):
value = index.model().data(index, QtCore.Qt.DisplayRole)
editor.setText(str(value))
def setModelData(self, editor, model, index):
model.setData(index, editor.text())
if not self.setModelDataEvent is None:
self.setModelDataEvent()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, app):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.model = Model(self)
self.ui.listView.setModel(self.model)
self.ui.listView.setItemDelegate(Delegate())
self.ui.listView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.ui.listView.customContextMenuRequested.connect(self.contextMenu)
self.ui.listView.setAlternatingRowColors(True)
self.ui.pushButton.clicked.connect(self.addItem)
self.ui.pushButton_2.clicked.connect(self.delItem)
def contextMenu(self, point):
self.menu = QtWidgets.QMenu(self)
self.menu.addAction('追加', self.addItem)
self.menu.addAction('削除', self.delItem)
self.menu.exec_( self.ui.listView.mapToGlobal(point) )
def addItem(self):
self.model.addItems([str(self.model.rowCount())])
def delItem(self):
if len(self.ui.listView.selectedIndexes()) == 0:
return
rows = [index.row() for index in self.ui.listView.selectedIndexes()]
self.model.removeItems(rows)
def main():
app = QtWidgets.QApplication(sys.argv)
window = MainWindow(app)
window.show()
app.exec_()
if __name__ == '__main__':
main()
mainwindow.py
## -*- coding: utf-8 -*-
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.setEnabled(True)
MainWindow.resize(300, 400)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
MainWindow.setSizePolicy(sizePolicy)
MainWindow.setMinimumSize(QtCore.QSize(300, 400))
MainWindow.setMaximumSize(QtCore.QSize(300, 400))
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(11)
self.pushButton.setFont(font)
self.pushButton.setObjectName("pushButton")
self.horizontalLayout.addWidget(self.pushButton)
self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(11)
self.pushButton_2.setFont(font)
self.pushButton_2.setObjectName("pushButton_2")
self.horizontalLayout.addWidget(self.pushButton_2)
self.verticalLayout.addLayout(self.horizontalLayout)
self.listView = QtWidgets.QListView(self.centralwidget)
font = QtGui.QFont()
font.setPointSize(11)
self.listView.setFont(font)
self.listView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.listView.setObjectName("listView")
self.verticalLayout.addWidget(self.listView)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 300, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.pushButton.setText(_translate("MainWindow", "Add"))
self.pushButton_2.setText(_translate("MainWindow", "Delete"))
mainwindow.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>400</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>400</height>
</size>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pushButton">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_2">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="listView">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>