Related
In this post, my goal is to concatenate two QFileSystemModels to one and display them together. (Lots of updates has been made)
Context :
In my C drive , I created the folder MyFolder (https://drive.google.com/drive/folders/1M-b2o9CiohXOgvjoZrAnl0iRVQBD1sXY?usp=sharing) , in which there are some folders and some files, for the sake of producing the minimal reproducible example . Their structure is :
The following Python code using PyQt5 library (modified from How to display parent directory in tree view?) runs after importing necessary libraries:
#The purpose of the proxy model is to display the directory.
#This proxy model is copied here from the reference without modification.
class ProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._root_path = ""
def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel()
if self._root_path and isinstance(source_model, QFileSystemModel):
root_index = source_model.index(self._root_path).parent()
if root_index == source_parent:
index = source_model.index(source_row, 0, source_parent)
return index.data(QFileSystemModel.FilePathRole) == self._root_path
return True
#property
def root_path(self):
return self._root_path
#root_path.setter
def root_path(self, p):
self._root_path = p
self.invalidateFilter()
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.create_treeview()
self.setCentralWidget(self.treeView_1) #The line I will be talking about.
def create_treeview(self):
self.treeView_1 = QTreeView()
self.dirModel_1 = QFileSystemModel()
self.dirModel_1.setRootPath(QDir.rootPath())
path_1 = 'C:/MyFolder/SubFolder1' # Changing the path is sufficient to change the displayed directory
root_index_1 = self.dirModel_1.index(path_1).parent()
self.proxy_1 = ProxyModel(self.dirModel_1)
self.proxy_1.setSourceModel(self.dirModel_1)
self.proxy_1.root_path = path_1
self.treeView_1.setModel(self.proxy_1)
proxy_root_index_1 = self.proxy_1.mapFromSource(root_index_1)
self.treeView_1.setRootIndex(proxy_root_index_1)
self.treeView_2 = QTreeView()
self.dirModel_2 = QFileSystemModel()
self.dirModel_2.setRootPath(QDir.rootPath())
path_2 = 'C:/MyFolder'
root_index_2 = self.dirModel_2.index(path_2).parent()
self.proxy_2 = ProxyModel(self.dirModel_2)
self.proxy_2.setSourceModel(self.dirModel_2)
self.proxy_2.root_path = path_2
self.treeView_2.setModel(self.proxy_2)
proxy_root_index_2 = self.proxy_2.mapFromSource(root_index_2)
self.treeView_2.setRootIndex(proxy_root_index_2)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
The line self.setCentralWidget(self.treeView_1) gives:
Changing self.setCentralWidget(self.treeView_1) to self.setCentralWidget(self.treeView_2) gives:
Objective:
My goal is to concatenate the two trees together. That is, when click run, the user should be able to see:
The order which they show up does not matter. All I care is that MyFolder and SubFolder1 show up as if they are completely independent items (even though in reality one is a subfolder of the other). I should remark that everything is static. That is, we are not trying to detect any changes on folders or files. The only time we ever need to peak at the existing folders and files will be when we click on run.
Update:
After several days of studying and trying, a major progress has been made. I thank musicamante for the hint of using QTreeWidget. The idea is, as said in comments, traverse through models and gradually move everything into one new QTreeWidget. To avoid freeze, my solution is to ask the QFileSystemModel to fetchMore whenever the user wants to see more (i.e. when the user wants to extend QTreeWidget).
The following code runs and almost solves my problem:
import os
from PyQt5.QtCore import*
from PyQt5.QtWidgets import*
from PyQt5 import QtTest
class To_Display_Folder(QSortFilterProxyModel):
def __init__(self, disables=False, parent=None):
super().__init__(parent)
#self.setFilterRegularExpression(r'^(.*\.dcm|[^.]+)$')
self._disables = bool(disables)
self._root_path = ""
def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel()
#case 1 folder
if self._root_path and isinstance(source_model, QFileSystemModel):
root_index = source_model.index(self._root_path).parent()
if root_index == source_parent:
index = source_model.index(source_row, 0, source_parent)
return index.data(QFileSystemModel.FilePathRole) == self._root_path
return True
'''
#case 2 file
file_index = self.sourceModel().index(source_row, 0, source_parent)
if not self._disables:
return self.matchIndex(file_index)
return file_index.isValid()
'''
#property
def root_path(self):
return self._root_path
#root_path.setter
def root_path(self, p):
self._root_path = p
self.invalidateFilter()
def matchIndex(self, index):
return (self.sourceModel().isDir(index) or
super().filterAcceptsRow(index.row(), index.parent()))
def flags(self, index):
flags = super().flags(index)
if (self._disables and
not self.matchIndex(self.mapToSource(index))):
flags &= ~Qt.ItemIsEnabled
return flags
class Widget_Item_from_Proxy(QTreeWidgetItem):
def __init__(self, index_in_dirModel, parent = None):
super().__init__(parent)
self.setText(0, index_in_dirModel.data(QFileSystemModel.FileNameRole))
self.setText(1, index_in_dirModel.data(QFileSystemModel.FilePathRole))
if os.path.isfile(index_in_dirModel.data(QFileSystemModel.FilePathRole)):
self.setIcon(0,QApplication.style().standardIcon(QStyle.SP_FileIcon))
else:
self.setIcon(0,QApplication.style().standardIcon(QStyle.SP_DirIcon))
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
global treeWidget
treeWidget = QTreeWidget()
self.treeWidget = treeWidget
self.treeWidget.itemExpanded.connect(self.upon_expansion)
self.treeWidget.itemClicked.connect(self.tree_click)
#The following directories will be displayed on the tree.
self.add_path_to_tree_widget('C:/MyFolder')
self.add_path_to_tree_widget('C:/Users/r2d2w/OneDrive/Desktop')
self.add_path_to_tree_widget('C:/')
self.setCentralWidget(self.treeWidget)
def add_path_to_tree_widget(self,path):
dirModel = QFileSystemModel()
dirModel.setRootPath(QDir.rootPath())
dirModel.directoryLoaded.connect(lambda: self.once_loaded(path, dirModel))
def once_loaded(self, path, dirModel):
if dirModel.canFetchMore(dirModel.index(path)):
dirModel.fetchMore(dirModel.index(path))
return
root_index = dirModel.index(path).parent()
proxy = To_Display_Folder(disables = False, parent = dirModel)
proxy.setSourceModel(dirModel)
proxy.root_path = path
proxy_root_index = proxy.mapFromSource(root_index)
origin_in_proxy = proxy.index(0,0,parent = proxy_root_index)
root_item = Widget_Item_from_Proxy(
proxy.mapToSource(origin_in_proxy))
self.treeWidget.addTopLevelItem(root_item)
for row in range(0, proxy.rowCount(origin_in_proxy)):
proxy_index = proxy.index(row,0,parent = origin_in_proxy)
child = Widget_Item_from_Proxy(
proxy.mapToSource(proxy_index),
parent = self.treeWidget.topLevelItem(self.treeWidget.topLevelItemCount()-1))
dirModel.directoryLoaded.disconnect()
#pyqtSlot(QTreeWidgetItem)
def upon_expansion(self, treeitem):
for i in range(0, treeitem.childCount()):
if os.path.isdir(treeitem.child(i).text(1)):
self.add_child_path_to_tree_widget(treeitem.child(i))
def add_child_path_to_tree_widget(self,subfolder_item):
subfolder_path = subfolder_item.text(1)
dirModel = QFileSystemModel()
dirModel.setRootPath(QDir.rootPath())
dirModel.directoryLoaded.connect(lambda: self.child_once_loaded(subfolder_item, subfolder_path,dirModel))
def child_once_loaded(self, subfolder_item, subfolder_path, dirModel):
if dirModel.canFetchMore(dirModel.index(subfolder_path)):
dirModel.fetchMore(dirModel.index(subfolder_path))
return
root_index = dirModel.index(subfolder_path).parent()
proxy = To_Display_Folder(disables = False, parent = dirModel)
proxy.setSourceModel(dirModel)
proxy.root_path = subfolder_path
proxy_root_index = proxy.mapFromSource(root_index)
origin_in_proxy = proxy.index(0,0,parent = proxy_root_index)
root_item = Widget_Item_from_Proxy(
proxy.mapToSource(origin_in_proxy))
folder_item = subfolder_item.parent()
itemIndex = folder_item.indexOfChild(subfolder_item)
folder_item.removeChild(subfolder_item)
folder_item.insertChild(itemIndex, root_item)
for row in range(0, proxy.rowCount(origin_in_proxy)):
proxy_index = proxy.index(row,0,parent = origin_in_proxy)
child = Widget_Item_from_Proxy(
proxy.mapToSource(proxy_index),
parent = root_item)
dirModel.directoryLoaded.disconnect()
#pyqtSlot(QTreeWidgetItem)
def tree_click(self, item):
print(item.text(0))
print(item.text(1))
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Since the bounty period is still not over, I will use the time to post two new questions:
Sometimes, especially when the line self.add_path_to_tree_widget('C:/') is present, the code does not give all directories when we click run. This problem is easily fixed by closing the window and clicking on run again. This problem occurs because the QFileSystemModel does not yet have enough time to traverse through the designated folder. If it has just a little bit more time, it will be able to. I wonder if there is a way to fix this programatically.
The function add_path_to_tree_widget is similar to add_child_path_to_tree_widget. The function once_loaded is similar to child_once_loaded. I wonder if there is a way to write these functions more succinctly.
While not impossible, it's quite difficult to create a unique and dynamic model that is able to access different QFileSystemModel structures.
An easier and simpler implementation, which would be more practical for static purposes, is to use a QTreeWidget and create items recursively.
class MultiBrowser(QTreeWidget):
def __init__(self, *pathList):
super().__init__()
self.iconProvider = QFileIconProvider()
self.setHeaderLabels(['Name'])
for path in pathList:
item = self.createFSItem(QFileInfo(path), self.invisibleRootItem())
self.expand(self.indexFromItem(item))
def createFSItem(self, info, parent):
item = QTreeWidgetItem(parent, [info.fileName()])
item.setIcon(0, self.iconProvider.icon(info))
if info.isDir():
infoList = QDir(info.absoluteFilePath()).entryInfoList(
filters=QDir.AllEntries | QDir.NoDotAndDotDot,
sort=QDir.DirsFirst
)
for childInfo in infoList:
self.createFSItem(childInfo, item)
return item
# ...
multiBrowser = MultiBrowser('path1', 'path2')
For obvious reasons, the depth of each path and their contents will freeze the UI from interaction until the whole structure has been crawled.
If you need a more dynamic approach, consider using the QFileSystemModel as a source for path crawling, along with its directoryLoaded signal, which will obviously require a more complex implementation.
How to show hidden row after applying filter in QTableView. I attached the code below and I applied filter for second column for filter value '2'. it is working as required. if want to show hidden row which contain value '3' in second column. it is not showing the row. I used match command to find row. everything working fine. but row not showing. please help me to resolve this.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class TableModel(QAbstractTableModel):
def __init__(self, data):super().__init__();self._data = data
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole or role == Qt.EditRole :return self._data[index.row()][index.column()]
def rowCount(self, index):return len(self._data)
def columnCount(self, index):return len(self._data[0])
class tableview(QTableView):
def __init__(self):
super().__init__()
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.horizontalHeader().setStyleSheet("::section{Background-color:lightgray;border-radius:10px;}")
self.smodel = QSortFilterProxyModel()
self.smodel.setFilterKeyColumn(1)
self.setModel(self.smodel)
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.smodel.setSourceModel(TableModel([[1,2],[1,2],[1,3],[1,4]]))
self.smodel.setFilterFixedString('2')
def find(self,key):
start = self.smodel.index(0, 1)
matches = self.smodel.sourceModel().match(start,Qt.DisplayRole,key,hits=-1,flags=Qt.MatchExactly)
for match in matches:self.showRow(match.row())
app = QApplication([])
table=tableview()
table.show()
b=QPushButton();b.clicked.connect(lambda:table.find('3'))
b.show()
app.exec_()
Current result
Required result on button press
I think filter and showRow()/hideRow() can work in different way - so they may have problem to work together.
Filter removes data before sending to TableView, showRow()/hideRow() removes row directly in TableView. If you want to use showRow then you may need to clear filter, hide all rows and show rows with 2 and 3
But it may be simpler to use filter
To show only rows with selected value (key = "3")
smodel.setFilterFixedString(key)
To clear filter and show all rows
smodel.setFilterFixedString("")
To filter few values you can use regex
self.smodel.setFilterRegExp("2|3")
or you could keep values on list
filtered = ["2", "3"]
self.smodel.setFilterRegExp( "|".join(filtered) )
Minimal working code.
My button toggles row "3" - first click shows row, second click hides row, etc.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole or role == Qt.EditRole :
return self._data[index.row()][index.column()]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
class tableview(QTableView):
def __init__(self):
super().__init__()
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.horizontalHeader().setStyleSheet("::section{Background-color:lightgray;border-radius:10px;}")
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.smodel = QSortFilterProxyModel()
self.setModel(self.smodel)
self.smodel.setSourceModel(TableModel([[1,2],[1,2],[1,3],[1,4]]))
self.smodel.setFilterKeyColumn(1)
self.filtered = ["2"]
#self.smodel.setFilterFixedString("2")
self.smodel.setFilterRegExp( "|".join(self.filtered) )
def find(self, key):
print('find:', key)
if key in self.filtered:
self.filtered.remove(key)
else:
self.filtered.append(key)
#self.smodel.setFilterFixedString("") # clear filter - show all rows
#self.smodel.setFilterFixedString(key)
#self.smodel.setFilterRegExp("2|3")
self.smodel.setFilterRegExp( "|".join(self.filtered) )
# --- main ---
app = QApplication([])
table = tableview()
table.show()
button = QPushButton(text="Toggle: 3")
button.clicked.connect(lambda:table.find('3'))
button.show()
app.exec()
BTW:
I see only one problem: some chars have special meaning in regex so adding ie. dot . to filtered may hide all rows, so it may need use \..
The same problem can be with | ( ) [ ] ^ $, etc.
I am writing a tool that allows me to track some tasks along a path of predifined stages, from something on a backlog, to ToDo, through WIP, Review and finally to done.
I created a custom widget, that will eventually be yellow, not unlike a postit note and perhaps with a bit of formatting it to give it a nice frame, etc... but stopped before getting far enough to make it look right because of this issue.
The idea is that each of these yellow Task widgets will have a stage they are at, and that I can select them in a Table Widget, and move them onto the next or previous stage, which will update taht objects stage, then refresh the TableWidget, read all the widget and where thay should be and set them in their new place.
So I have it kind of working to some degree (below), where I can move the tasks forward and they update location, but I noticed when I click the cells that the widget was previously in, print statement still says that the cell still has a widget there (which kind of makes sense, as code below isn't removing the previous one, but I'd expect to visually still see it). And I can move them forward and backwards, and the information on the tasks does update correctly, but the table won't refresh unless the task moves to a cell that never had a cellWidget in it. Test this by moving it backwards. It works, movnig forward visually does nothing, but moving again, does show up.
I tried clearing the TableWidget and rebuilding from scratch and that crashes. The main issue I am having is that with all these crashes, which is an issue in itself as it makes debugging very tough... When I try and clear the TableWidget (with .clear()) before repopulating, I get this.
Process finished with exit code -1073741819 (0xC0000005)
Same error code if I try removing the old cells by setting the Table Widget to 0 rows before adding the correct number of rows.
A known issue that is less important is when I select a cell without a widget and try and move it, gies me this, but don't worry too much about that fix, as it's known issue.
Process finished with exit code -1073740791 (0xC0000409)
Also tried cleaning up by iterating every cell and if it has a cell widget, remove cell widget before re-setting them to correct place and it still crashes. I'm out of ideas.
Task Widget
import sys
from PyQt5.QtWidgets import (QApplication, QTableWidget, QWidget, QFrame, QHBoxLayout, QLabel,
QPushButton,QVBoxLayout)
class Task(QWidget):
def __init__(self, ID, name, est):
super(Task, self).__init__()
# Creates a small widget that will be added to a table widget
self.ID = ID
self.name = name
self.est = est
# These cell widgets represent tasks. So each task has a particular 'stage' it is at
self.stage = 'ToDo'
self.stages = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
self.objects_labels = {}
self.initUI()
def initUI(self):
# adds a bunch of labels to the widget
layout = QVBoxLayout()
frame = QFrame()
frame.setFrameShape(QFrame.StyledPanel)
frame.setStyleSheet('background-color: red')
frame.setLineWidth(2)
layout.addWidget(frame)
info = [self.ID, self.name, self.est]
for section in info:
self.objects_labels[section] = QLabel(str(section))
layout.addWidget(self.objects_labels[section])
self.setLayout(layout)
self.setStyleSheet('background-color: yellow')
def task_move(self, forward = True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
print(self.stages)
print(index)
if forward:
print('--->')
if self.stage == self.stages[-1]:
print('Already at the end of process')
return
self.stage = self.stages[index + 1]
else:
print('<---')
if self.stage == self.stages[0]:
print('Already at the start of process')
return
self.stage = self.stages[index - 1]
MainWidget
class MainWidget(QWidget):
def __init__(self):
super().__init__()
self.tasks = self.make_tasks()
self.init_ui()
self.update_tw()
def make_tasks(self):
# Create a few tasks
a = Task(0, 'Name_A', 44)
b = Task(0, 'Name_B', 22)
c = Task(0, 'Name_C', 66)
d = Task(0, 'Name_D', 90)
return [a, b, c, d]
def init_ui(self):
layout_main = QVBoxLayout()
self.tw = QTableWidget()
self.tw.cellClicked.connect(self.cell_clicked)
self.tw.horizontalHeader().setDefaultSectionSize(120)
self.tw.verticalHeader().setDefaultSectionSize(120)
layout_main.addWidget(self.tw)
layout_bottom_button_bar = QHBoxLayout()
self.btn_task_backward = QPushButton('<--- Task')
self.btn_task_backward.clicked.connect(lambda: self.move_task(forward=False))
self.btn_task_forward = QPushButton('Task --->')
self.btn_task_forward.clicked.connect(lambda: self.move_task())
for widget in [self.btn_task_backward, self.btn_task_forward]:
layout_bottom_button_bar.addWidget(widget)
layout_main.addLayout(layout_bottom_button_bar)
self.setLayout(layout_main)
self.setGeometry(300, 300, 800, 600)
self.setWindowTitle('MainWidget')
self.show()
#property
def tw_header(self):
return {'Backlog': 0, 'ToDo': 1, 'WIP': 2, 'Review': 3, 'Done': 4}
#property
def selected_indices(self):
return [(x.row(), x.column()) for x in self.tw.selectedIndexes()]
#property
def selected_widgets(self):
selected_widgets = [self.tw.cellWidget(x[0], x[1]) for x in self.selected_indices]
print(selected_widgets)
return selected_widgets
def move_task(self, forward=True):
# Crashes if you select a non-widget cell, but thats a known issue
# Moves the task forward or backward and then prompts to update the TableWidget
for object in self.selected_widgets:
object.task_move(forward=forward)
self.tw.clearSelection()
self.update_tw()
def cell_clicked(self, row, column):
if self.tw.cellWidget(row, column):
print(self.selected_indices)
print(self.selected_widgets)
else:
print('No Cell Widget here')
def update_tw(self):
#I wanted to clear the Table widget and rebuild, but this crashes
# self.tw.clear()
self.tw.setHorizontalHeaderLabels(self.tw_header.keys())
rows = len(self.tasks)
columns = len(self.tw_header)
self.tw.setRowCount(rows)
self.tw.setColumnCount(columns)
# Looks through each task, and then gets it's stage, and then adds the widget to the correct column
for index, object in enumerate(self.tasks):
column = self.tw_header[object.stage]
print('Setting stage {} for {}\n...to r={}, c={}\n***'.format(object.stage, object, index, column))
self.tw.setCellWidget(index, column, object)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWidget()
sys.exit(app.exec_())
From my previous experience, I always found using setCellWidget clunky, underperforming and buggy.
Most of the times my Widgets were lost or misplaced, while refreshing the table similarly to the way you are doing it.
In addition, I guess you would want to use this "Task Mover" on a larger scale, and from what I could see, setting separate Widgets inside QWidgetItems becomes quite slow when done on loads of items.
My suggestion would be to use style delegates, so that you can customize the look of your items to your liking, without having to deal with the setCellWidget stuff which is giving you problem.
Once you have your own delegate, and paint the items the way you want, you can just keep updating that item data and moving the items around the table by using "take" and "set".
I am not sure if this would be the best way of executing this specific task, but moving towards this direction would probably give you greater flexibility and customisation power in the long run.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class TaskProperty():
properties = ["ID", "name", "est", "stage"]
count = 4
ID, Name, Est, Stage = [Qt.UserRole + x for x in range(count)]
STAGES = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
class MainWidget(QWidget):
def __init__(self):
super(MainWidget, self).__init__()
self.tasks = self.make_tasks()
self.init_ui()
self.update_tw()
def make_tasks(self):
# Create a few tasks
a = Task(0, 'Name_A', 44)
b = Task(0, 'Name_B', 22)
c = Task(0, 'Name_C', 66)
d = Task(0, 'Name_D', 90)
return [a, b, c, d]
def init_ui(self):
layout_main = QVBoxLayout()
self.tw = QTableWidget()
# create and set the delegate to the TableWidget
self.delegate = TaskDelegate(self.tw )
self.tw.setItemDelegate(self.delegate)
self.tw.cellClicked.connect(self.cell_clicked)
self.tw.horizontalHeader().setDefaultSectionSize(120)
self.tw.verticalHeader().setDefaultSectionSize(120)
layout_main.addWidget(self.tw)
layout_bottom_button_bar = QHBoxLayout()
self.btn_task_backward = QPushButton('<--- Task')
self.btn_task_backward.clicked.connect(lambda: self.move_task(forward=False))
self.btn_task_forward = QPushButton('Task --->')
self.btn_task_forward.clicked.connect(lambda: self.move_task())
for widget in [self.btn_task_backward, self.btn_task_forward]:
layout_bottom_button_bar.addWidget(widget)
layout_main.addLayout(layout_bottom_button_bar)
self.setLayout(layout_main)
self.setGeometry(300, 300, 800, 600)
self.setWindowTitle('MainWidget')
self.show()
#property
def tw_header(self):
return {'Backlog': 0, 'ToDo': 1, 'WIP': 2, 'Review': 3, 'Done': 4}
#property
def selected_indices(self):
return [(x.row(), x.column()) for x in self.tw.selectedIndexes()]
def move_task(self, forward=True):
'''
To move the task to the next step, we iterate all the items selected.
If the task can be moved, we take the corresponding item from its current cell and move it to the destination.
:param forward:
:return:
'''
selected =self.tw.selectedItems()
for item in selected:
item.setSelected(False)
result = item.task_move(forward=forward)
if result:
next = 1 if forward else -1
row = item.row()
column = item.column()
moveItem = self.tw.takeItem(row, column)
self.tw.setItem(row, column + next, moveItem)
moveItem.setSelected(True)
def cell_clicked(self, row, column):
item = self.tw.item(row, column)
if not isinstance(item, TaskItem):
print "No Task Item Here"
def update_tw(self):
# I wanted to clear the Table widget and rebuild, but this crashes
# self.tw.clear()
self.tw.clear()
self.tw.setHorizontalHeaderLabels(self.tw_header.keys())
rows = len(self.tasks)
columns = len(self.tw_header)
self.tw.setRowCount(rows)
self.tw.setColumnCount(columns)
# Looks through each task, and then gets it's stage, and then adds the widget to the correct column
for row, object in enumerate(self.tasks):
# create items of our custom type only for the column that need to be filled.
# the other cells will be filled with null items.
column = STAGES.index(object.stage)
print('Setting stage {} for {}\n...to r={}, c={}\n***'.format(object.stage, object, row, column))
item = TaskItem(object)
self.tw.setItem(row, column, item)
class TaskDelegate(QStyledItemDelegate):
'''
This delegate take care of Drawing our cells the way we want it to be.
'''
def paint(self, painter, option, index):
'''
Override the Paint function to draw our own cell.
If the QTableWidgetItem does not have our Data stored in it, we do a default paint
:param painter:
:param option:
:param index:
:return:
'''
painter.save()
rect = option.rect
status = index.data(TaskProperty.Stage)
if status is None:
return super(TaskDelegate, self).paint(painter, option, index)
else:
id = STAGES.index(status)
pen = painter.pen()
pen.setBrush(Qt.black)
painter.setPen(pen)
if id == index.column():
rect.translate(3, 3)
newRect = QRect(rect.x(), rect.y(), rect.width() - 6, 20)
infos = [index.data(TaskProperty.ID), index.data(TaskProperty.Name), index.data(TaskProperty.Est)]
painter.setBrush(Qt.red)
painter.drawRect(newRect)
painter.setBrush(Qt.yellow)
for info in infos:
newRect.translate(0, 25)
painter.drawRect(newRect)
painter.drawText(newRect, Qt.AlignHCenter | Qt.AlignVCenter,
str(info))
class TaskItem(QTableWidgetItem):
'''
Subclass QTableWidgetItem.
Probably not needed, since we can set the property when we create the item instead of in the init,
and keep track of which item is attached to which task object using the Column Index of the table.
However, this can be useful if you want to attach more specific procedures to your items
'''
def __init__(self, task):
super(TaskItem, self).__init__()
self._task = task
self.setData(TaskProperty.ID, task.ID)
self.setData(TaskProperty.Name, task.name)
self.setData(TaskProperty.Est, task.est)
self.setData(TaskProperty.Stage, task.stage)
self.objects_labels = {}
def task_move(self, forward=True):
result = self._task.task_move(forward=forward)
self.setData(TaskProperty.Stage, self._task.stage)
return result
class Task(object):
'''
The Task class is now just an object, not a widget.
'''
def __init__(self, ID, name, est):
# Creates a small widget that will be added to a table widget
self.ID = ID
self.name = name
self.est = est
# These cell widgets represent tasks. So each task has a particular 'stage' it is at
self.stage = 'ToDo'
self.stages = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
self.objects_labels = {}
def task_move(self, forward=True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
if forward:
print('--->')
if self.stage == self.stages[-1]:
#print('Already at the end of process')
return False
self.stage = self.stages[index + 1]
else:
print('<---')
if self.stage == self.stages[0]:
#print('Already at the start of process')
return False
self.stage = self.stages[index - 1]
return True
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWidget()
sys.exit(app.exec_())
It is not necessary to clean and create everything again, instead just move the widget for it we must know if it can be moved or not and for that task_move must indicate if the movement is valid or not. Considering the above, the solution is:
def task_move(self, forward=True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
print(self.stages)
print(index)
if forward:
print("--->")
if self.stage == self.stages[-1]:
print("Already at the end of process")
return False
self.stage = self.stages[index + 1]
else:
print("<---")
if self.stage == self.stages[0]:
print("Already at the start of process")
return False
self.stage = self.stages[index - 1]
return True
def move_task(self, forward=True):
for row, column in self.selected_indices:
widget = self.tw.cellWidget(row, column)
if isinstance(widget, Task) and widget.task_move(forward):
next_column = column + (1 if forward else -1)
# create new task widget
task = Task(widget.ID, widget.name, widget.est)
# remove all task widget
self.tw.removeCellWidget(row, column)
# move task widget
self.tw.setCellWidget(row, next_column, task)
self.tw.clearSelection()
The crashed is because when using clear you are also removing the Task widget so "self.tasks" has objects deleted from C++ that you should not use.
Is is a piece of my code . I have two classes CheckerScene and Checkers . CHesckers - is my main window . I can't realize EndGameSignal defined in CheckerScene class . When it emits , pySlot can't catch it in class Checkers , as i want . When my EndGameSignal emmits - i want to see a dialog message on my main screen (pyQtSlots functions realized in my code), not on the scene . How can i correct my program to do it .
class CheckerScene(QtWidgets.QGraphicsScene):
EndGameSignal=QtCore.pyqtSignal('QString')
def init(self):
QtWidgets.QGraphicsScene.init(self)
# scene congifuratios
self.setSceneRect(margin, margin, gridCount * gridSlotSize, gridCount * gridSlotSize)
self.addRect(self.sceneRect())
# create signal . It will be emit() from blackboard.crash()
self.signaldel.connect(self.del_item)
#choosing the visual checker and its coordinates
self.current = None
#list of grids and checkers
self.grid = []
self.white_checkers = []
self.black_checkers = []
for row in range(8):
for column in range(8):
# this is a "trick" to make the grid creation easier: it creates
# a grid square only if the row is odd and the column is even,
# and viceversa.
if (not row & 1 and column & 1) or (row & 1 and not column & 1):
# create a gridItem with a rectangle that uses 0-based
# coordinates, *then* we set its position
gridItem = self.addRect(0, 0, gridSlotSize, gridSlotSize)
gridItem.setPos(margin + column * gridSlotSize, margin + row * gridSlotSize)
gridItem.setBrush(QtGui.QColor(QtCore.Qt.lightGray))
self.grid.append(gridItem)
if 3 <= row <= 4:
# don't add checkers in the middle
continue
# create checkers being careful to assign them the gridItem
# as a *parent*; their coordinate will *always* be relative
# to the parent, so that if we change it, they will always
# be centered
if row < 3:
self.black_checkers.append(CheckerItem(0, gridItem))#!
else:
self.white_checkers.append(CheckerItem(1, gridItem))#!
self.additionsl__init__()
self.EndGameSignal.connect(Checkers.handler_EndGameSignal)
self.EndGameSignal.emit('=NAME')
class Checkers(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.Initialization()
def Initialization(self):
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.player2Label = QtWidgets.QLabel('Player 2')
layout.addWidget(self.player2Label)
self.player2Label.setAlignment(QtCore.Qt.AlignCenter)
self.checkerView = QtWidgets.QGraphicsView()
layout.addWidget(self.checkerView)
self.checkerScene = CheckerScene()
self.checkerView.setScene(self.checkerScene)
self.checkerView.setFixedSize(gridSize, gridSize)
# set the Antialiasing render hints to paint "smoother" shapes
self.checkerView.setRenderHints(QtGui.QPainter.Antialiasing)
self.player1Label = QtWidgets.QLabel('Player 1')
layout.addWidget(self.player1Label)
self.player1Label.setAlignment(QtCore.Qt.AlignCenter)
#QtCore.pyqtSlot(str)
def handler_EndGameSignal(self, result):
result=QtWidgets.QMessageBox.question(self,f"Выиграл {result}","Сиграть еще раз ?",QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No,QtWidgets.QMessageBox.No)
if result == QtWidgets.QMessageBox.Yes :
self.close()
else :
pass
print(f"WINNER {result}")
#QtCore.pyqtSlot(bool)
def handler_EndGameSignal(self, result):
result = QtWidgets.QMessageBox.question(self, f"НИЧЬЯ !", "Сиграть еще раз ?",
QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
if result == QtWidgets.QMessageBox.Yes:
self.close()
else:
pass
print("DRAW")
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
checkers = Checkers()
checkers.show()
sys.exit(app.exec_())
As with your previous question, you're still confusing classes and instancies. In your code you connect the signal to the class, while you have to connect it to the instance.
Since you have no reference with the receiver (the Checker instance) from the sender (the scene), you'll have to connect it from the former:
class Checkers(QtWidgets.QWidget):
def Initialization(self):
# ...
self.checkerScene.EndGameSignal.connect(self.handler_EndGameSignal)
I am super new to Qt programming. I am trying to make a simple table that can have rows added by clicking a button. I can implement the table fine but can't seem to get the updated data to show on the table. I believe my problem stems from the fact that I can't seem to properly call any sort of "change data" method using the button. I've tried several different solutions online all of which have lead to 4 year old, dead-end posts. What I have so far is the basic structure, I just can't figure out how to make the table update with new data.
This is the basic view
I have set up with some test data.
In the final implementation, the table will start empty and I would like to append rows and have them displayed in the table view.
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
# create table
self.get_table_data()
self.table = self.createTable()
# layout
self.layout = QVBoxLayout()
self.testButton = QPushButton("test")
self.connect(self.testButton, SIGNAL("released()"), self.test)
self.layout.addWidget(self.testButton)
self.layout.addWidget(self.table)
self.setLayout(self.layout)
def get_table_data(self):
self.tabledata = [[1234567890,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20]]
def createTable(self):
# create the view
tv = QTableView()
# set the table model
header = ['col_0', 'col_1', 'col_2', 'col_3', 'col_4']
tablemodel = MyTableModel(self.tabledata, header, self)
tv.setModel(tablemodel)
# set the minimum size
tv.setMinimumSize(400, 300)
# hide grid
tv.setShowGrid(False)
# hide vertical header
vh = tv.verticalHeader()
vh.setVisible(False)
# set horizontal header properties
hh = tv.horizontalHeader()
hh.setStretchLastSection(True)
# set column width to fit contents
tv.resizeColumnsToContents()
# set row height
tv.resizeRowsToContents()
# enable sorting
tv.setSortingEnabled(False)
return tv
def test(self):
self.tabledata.append([1,1,1,1,1])
self.emit(SIGNAL('dataChanged()'))
print 'success'
class MyTableModel(QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None):
"""
Args:
datain: a list of lists\n
headerdata: a list of strings
"""
QAbstractTableModel.__init__(self, parent)
self.arraydata = datain
self.headerdata = headerdata
def rowCount(self, parent):
return len(self.arraydata)
def columnCount(self, parent):
if len(self.arraydata) > 0:
return len(self.arraydata[0])
return 0
def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])
def setData(self, index, value, role):
pass # not sure what to put here
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.headerdata[col])
return QVariant()
def sort(self, Ncol, order):
"""
Sort table by given column number.
"""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
if order == Qt.DescendingOrder:
self.arraydata.reverse()
self.emit(SIGNAL("layoutChanged()"))
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
When the underlying data of the model changes, the model should emit either layoutChanged or layoutAboutToBeChanged, so that view updates properly (there's also dataChanged, if you want to update a specific range of cells).
So you just need something like this:
def test(self):
self.tabledata.append([1,1,1,1,1])
self.table.model().layoutChanged.emit()
print 'success'
QAbstractTableModel have two special methods for that ( beginInsertRows() and endInsertRows()).
You can add api-point in your custom model. For example:
def insertGuest(self, guest):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self.guestsTableData.append(guest)
self.endInsertRows()
I've made your table reference a class variable instead of an instance variable, so you could edit the data for the table from virtually anywhere in your code.
# First access the data of the table
self.tv_model = self.tv.model()
Secondly, I use the sort of pandas-dataframe-editing type approach.
Lets say your data that you want to add is stored in a variable on its own:
# These can be whatever, but for consistency,
# I used the data in the OP's example
new_values = [1, 1, 1, 1, 1]
There are different ways the next step can be approached, depending on whether the data is being added to the table, or updating existing values. Adding the data as a new row would be as follows.
# The headers should also be a class variable,
# but I left it as the OP had it
header = ['col_0', 'col_1', 'col_2', 'col_3', 'col_4']
# There are multiple ways of establishing what the row reference should be,
# this is just one example how to add a new row
new_row = len(self.tv_model.dataFrame.index)
for i, col in enumerate(header):
self.tv_model.dataFrame.loc[new_row, col] = new_values[i]
Since self.tv_model is a reference to the actual data of the table,
emitting the following signal will update the data, or 'commit' it to the model,
so to speak.
self.tv_model.layoutChanged.emit()