PyQt5 QListViewのサンプル

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で追加する処理に加えて、追加したというのをbeginInsertRowsendInsertRowsで知らせてあげる必要があります。 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でリストから削除してbeginRemoveRowsendRemoveRowsで削除を知らせてあげます。最初の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>

コメント

タイトルとURLをコピーしました