Some context:
I made a QTreeView and set QFileSystemModel as its model. There are 2 TreeViews, the one on the right corresponds in the list of the project files in a directory, and the left one serves for the view of the connected files for that project file. With help, I've implemented this behavior where when I click on a project file on the right side, its connected files will appear on the left side. (I removed the mechanism of storing and the connected files as it will add a ton of code)
The problem:
After adding the 2 TreeViews above, I then added a context menu with only one option, which is to delete the selected files. I think my first attempt which is on the left side is successful because I can see the path printed in the console. Now the problem lies when I tried to do it on the right side. Knowing that the model in this side is subclassed from the QSortFilterProxyModel, I then added the sourceModel() so that it will return the QFileSystemModel. But when I tried to run the file, It just gives me an error. You can see my implementation below.
def deleteFile(self, event):
index_list = self.tree_sender.selectedIndexes()
for index in index_list:
if index.column() == 0:
if isinstance(self.tree_sender.model(), ModifiedQSortFilterProxyModel)
fileIn = self.tree_sender.model().sourceModel().fileInfo(index)
else:
fileIn = self.tree_sender.model().fileInfo(index)
# method above is also not working even I add sourceModel()
# It just ends the app imediately
path_to_delete = fileIn.absoluteFilePath()
print(path_to_delete)
My Testing Code:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QFileSystemModel, QHBoxLayout, \
QVBoxLayout, QPushButton, QListWidget, QListWidgetItem, QMenu, QAction, QAbstractItemView
from PyQt5.QtCore import QSortFilterProxyModel, Qt
from PyQt5.QtGui import QCursor
class ModifiedQSortFilterProxyModel(QSortFilterProxyModel):
fileInfo = None
con_files = None
def filterAcceptsRow(self, source_row, source_parent):
if not self.fileInfo:
return True
source_index = self.sourceModel().index(source_row, 0, source_parent)
info = self.sourceModel().fileInfo(source_index)
if self.fileInfo.absolutePath() != info.absolutePath():
return True
return info.fileName() in self.con_files
def setFilter(self, info, connected_files):
self.fileInfo = info
self.con_files = connected_files
self.invalidateFilter()
class FileSystemView(QWidget):
def __init__(self):
super().__init__()
appWidth = 800
appHeight = 300
self.setWindowTitle('File System Viewer')
self.setGeometry(300, 300, appWidth, appHeight)
dir_path = r'<your directory>'
# -- left -- #
self.model = QFileSystemModel()
self.model.setRootPath(dir_path)
self.tree = QTreeView()
self.tree.setModel(self.model)
self.tree.setRootIndex(self.model.index(dir_path))
self.tree.setColumnWidth(0, 250)
self.tree.setAlternatingRowColors(True)
self.tree.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.tree.clicked.connect(self.onClicked)
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree.customContextMenuRequested.connect(self.context_menu)
# -- right -- #
self.model2 = QFileSystemModel()
self.model2.setRootPath(dir_path)
self.model2.setReadOnly(False)
self.filter_proxy_model = ModifiedQSortFilterProxyModel()
self.filter_proxy_model.setSourceModel(self.model2)
self.filter_proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
self.filter_proxy_model.setDynamicSortFilter(True)
self.filter_proxy_model.setFilterKeyColumn(0)
root_index = self.model2.index(dir_path)
proxy_index = self.filter_proxy_model.mapFromSource(root_index)
self.tree2 = QTreeView()
self.tree2.setModel(self.filter_proxy_model)
self.tree2.setRootIndex(proxy_index)
self.tree2.setColumnWidth(0, 250)
self.tree2.setAlternatingRowColors(True)
self.tree2.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.tree2.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree2.customContextMenuRequested.connect(self.context_menu)
# -- layout -- #
self.tree_layout = QHBoxLayout()
self.tree_layout.addWidget(self.tree)
self.tree_layout.addWidget(self.tree2)
self.setLayout(self.tree_layout)
def context_menu(self, event):
self.tree_sender = self.sender()
# ^^^ I dont know how to access the treeview where context menu
# ^^^ is triggered so I just tried to pass it as a variable
self.menu = QMenu(self)
deleteAction = QAction('Delete', self)
deleteAction.triggered.connect(lambda event: self.deleteFile(event))
self.menu.addAction(deleteAction)
self.menu.popup(QCursor.pos())
def deleteFile(self, event):
index_list = self.tree_sender.selectedIndexes()
for index in index_list:
if index.column() == 0:
if isinstance(self.tree_sender.model(), ModifiedQSortFilterProxyModel):
fileIn = self.tree_sender.model().sourceModel().fileInfo(index)
else:
fileIn = self.tree_sender.model().fileInfo(index)
# method above is also not working even I add sourceModel()
# It just ends the app imediately
path_to_delete = fileIn.absoluteFilePath()
print(path_to_delete)
def onClicked(self, index):
print("onclick")
connected_files = [] # list of connected files
self.filter_proxy_model.setFilter(self.model.fileInfo(index), connected_files)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())
My Question: What causes the error? is it sourceModel() implementation? or I did something very wrong?
I hope someone can point me out to the right direction.
I totally forgot to use mapToSource, but thankfully #musicamante guided me through it. My question is now solved, and the final implementation of the deleteFile method is given below.
def deleteFile(self, event):
index_list = self.tree_sender.selectedIndexes()
for index in index_list:
if index.column() == 0:
model = self.tree_sender.model()
if isinstance(model, ModifiedQSortFilterProxyModel):
proxy_index = model.mapToSource(index)
fileIn = proxy_index.model().fileInfo(proxy_index)
else:
fileIn = model.fileInfo(index)
path_to_delete = fileIn.absoluteFilePath()
print(path_to_delete)
Related
I am trying to create a tableWidget in PyQt that has copy-paste functionality and the ability to create new tabs. My table is loaded with a initialized sheet but gives the user the ability to create new tabs with new qTableWidgets which I handled using a for loop to create and initialize the widget everytime a new tab is created in my add_sheet() function.
I wanted to also add functionality for copying and pasting inside each tab and across the tabs. Now when I added the key press event function to do this, I kept getting errors when trying to copy and paste in new tabs as out of index. I tried to fix this by keeping a pointer of which tab the selected indexes come from but this only allows me to edit on the first new tab created. The initial spreadsheet crashes when trying to do operations and the other tabs just do not work. It also does not copy and paste universally amongst the tabs.
I feel I have made my handling too complicated and I have a flaw or am missing something in my design pattern.
How can I properly implement my copy-paste function to work amongst all dynamically tabs with their own QTableWidget instances including the initial QTableWidget created both locally and universally?
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
QTableWidget, QTableWidgetItem,QVBoxLayout,QTabWidget,QWidget,QToolButton,QToolBar)
from PyQt6.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.tab_widget = MyTabWidget(self)
self.setCentralWidget(self.tab_widget)
self.initializeUI()
def initializeUI(self):
"""Set up the application's GUI."""
self.setMinimumSize(1200, 500)
self.setWindowTitle("Spreadsheet - QTableWidget Example")
# Used for copy and paste actions
self.item_text = None
self.setUpMainWindow()
self.show()
def setUpMainWindow(self):
"""Create and arrange widgets in the main window."""
# Set initial row and column values
main_spreadsheet_widget.setRowCount(10)
main_spreadsheet_widget.setColumnCount(10)
class MyTabWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
# Initialize tab screen
self.sheets = QTabWidget()
self.main_sheet = QWidget()
self.sheets.resize(300, 200)
self.extra_sheets_tracker = list()
self.tab_index = []
self.copied_cells_list = []
self.paste_index = []
# Add sheets
self.sheets.addTab(self.main_sheet, "Main Sheet")
#self.sheets.addTab(self.tab3, "Geeks")
# Create first tab
self.main_sheet.layout = QVBoxLayout(self)
self.main_sheet.layout.addWidget(main_spreadsheet_widget)
self.main_sheet.setLayout(self.main_sheet.layout)
self.tabButton = QToolButton(self)
self.tabButton.setText('+')
font = self.tabButton.font()
font.setBold(True)
self.tabButton.setFont(font)
self.sheets.setCornerWidget(self.tabButton)
self.tabButton.clicked.connect(self.add_sheet)
# Add sheets to widget
self.layout.addWidget(self.sheets)
self.setLayout(self.layout)
def add_sheet(self):
self.sheet = QWidget()
self.main_tab_sheet_widget = QTableWidget()
self.extra_sheets_tracker.append(self.main_tab_sheet_widget)
self.main_tab_sheet_widget.setRowCount(10)
self.main_tab_sheet_widget.setColumnCount(10)
self.sheet.layout = QVBoxLayout(self)
self.sheet.layout.addWidget(self.main_tab_sheet_widget)
self.sheet.setLayout(self.sheet.layout)
self.sheets.addTab(self.main_tab_sheet_widget, "Sheet" + str(len(self.extra_sheets_tracker)))
def keyPressEvent(self, event):
super().keyPressEvent(event)
if event.key() == Qt.Key.Key_C and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
self.tab_index = []
for i in self.extra_sheets_tracker:
if i.selectedIndexes() is not None:
self.tab_index.append(self.extra_sheets_tracker.index(i))
self.copied_cells = sorted(self.extra_sheets_tracker[self.tab_index[0]].selectedIndexes())
self.copied_cells_list.append(self.copied_cells)
self.copied_cells = None
elif event.key() == Qt.Key.Key_V and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
self.paste_index = []
for i in self.extra_sheets_tracker:
self.paste_index.append(self.extra_sheets_tracker.index(i))
r = self.extra_sheets_tracker[self.paste_index[0]].currentRow() - self.copied_cells_list[0][0].row()
c = self.extra_sheets_tracker[self.paste_index[0]].currentColumn() - self.copied_cells_list[0][0].column()
for cell in self.copied_cells_list[0]:
self.extra_sheets_tracker[self.paste_index[0]].setItem(cell.row() + r, cell.column() + c, QTableWidgetItem(cell.data()))
if __name__ == "__main__":
app = QApplication(sys.argv)
main_spreadsheet_widget = QTableWidget()
window = MainWindow()
window.show()
sys.exit(app.exec())
I have managed to do the following:
1- I treated the main sheet as if it was "just another sheet" and made sure it follows the same relative routine. This included removing unnecessary functions like setUpMainWindow and introducing static values like MAX_ROWS and MAX_COLS for maximum rows and columns respectively. This also entailed renaming extra_sheets_tracker to become sheets_tracker since it will hold all of the sheets.
2- I have created two strategies for pasting: 1st-cell for selecting the first cell in a table, or 1-to-1 for selecting the exact count of cells at the pasting side. This meant, also, checking for the dimensions beforehand and raising the exception Unmatching pasting size if the sizes did not match.
3- I removed self.tab_index and self.copied_cells_list since they are not used. copy_list is used globally and it is not a bad idea to stay in self. However paste_list is not used anywhere else than the paste routine and I removed it from self. You need to look into your self variables, you seem to overuse them.
import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QVBoxLayout, QTabWidget, QWidget, QToolButton, QToolBar
from PyQt6.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.tab_widget = MyTabWidget(self)
self.setCentralWidget(self.tab_widget)
self.initializeUI()
def initializeUI(self):
"""Set up the application's GUI."""
self.setMinimumSize(1200, 500)
self.setWindowTitle("Spreadsheet - QTableWidget Example")
# Used for copy and paste actions
self.item_text = None
self.show()
class MyTabWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
# Initialize tab screen
self.MAX_ROWS = 10
self.MAX_COLS = 10
self.STRATEGY = {0:'1st-cell', 1:'1-to-1'}
self.sheets = QTabWidget()
self.main_sheet = QWidget()
self.sheets.resize(300, 200)
self.sheets_tracker = list()
self.copy_list = []
#self.copied_cells_list = []
#self.paste_list = []
# Add a sheet and create first tab
self.main_sheet.layout = QVBoxLayout(self)
# Setup main window
self.add_sheet()
tabButton = QToolButton(self)
tabButton.setText('+')
font = tabButton.font()
font.setBold(True)
tabButton.setFont(font)
tabButton.clicked.connect(self.add_sheet)
self.sheets.setCornerWidget(tabButton)
# Add sheets to widget
self.layout.addWidget(self.sheets)
self.setLayout(self.layout)
def add_sheet(self):
sheets_count = len(self.sheets_tracker)
if sheets_count < 1:
label = "Main sheet"
else:
label = f"Sheet{sheets_count}"
self.main_tab_sheet_widget = QTableWidget()
self.main_tab_sheet_widget.setRowCount(self.MAX_ROWS)
self.main_tab_sheet_widget.setColumnCount(self.MAX_COLS)
self.sheets_tracker.append(self.main_tab_sheet_widget)
self.sheet = QWidget()
self.sheet.layout = QVBoxLayout(self)
self.sheet.layout.addWidget(self.main_tab_sheet_widget)
self.sheet.setLayout(self.sheet.layout)
self.sheets.addTab(self.main_tab_sheet_widget, label)
def keyPressEvent(self, event):
super().keyPressEvent(event)
if event.key() == Qt.Key.Key_C and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
self.copy_list = sorted([(e.row(), e.column(), e.data()) for e in self.sheets_tracker[self.sheets.currentIndex()].selectedIndexes()])
if len(self.copy_list) != 0:
# recalculate the indicies to absolute values
least_row = self.copy_list[0][0]
least_col = sorted(self.copy_list, key=lambda tup: tup[1])[0][1]
self.copy_list = [(e[0]-least_row, e[1]-least_col, e[2]) for e in self.copy_list]
print(self.copy_list)
elif event.key() == Qt.Key.Key_V and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
paste_list = sorted([(element.row(), element.column(), element.data()) for element in self.sheets_tracker[self.sheets.currentIndex()].selectedIndexes()])
if len(paste_list) != 0:
if len(paste_list) == 1:
# The given paste position is the first cell only
current_r, current_c, _ = paste_list[-1]
last_r, last_c, _ = self.copy_list[-1]
if last_r + current_r - 1 < self.MAX_ROWS and last_c + current_c - 1 < self.MAX_COLS:
strategy = self.STRATEGY[0]
else:
raise Exception('Unmatching pasting size')
elif len(self.copy_list) == len(paste_list):
# You can paste in one-to-one correspondence
strategy = self.STRATEGY[1]
else:
raise Exception('Unmatching pasting size')
if strategy == self.STRATEGY[0]:
r, c, _ = paste_list[0]
for index, e in enumerate(self.copy_list):
_, _, d = self.copy_list[index]
d = '' if d is None else d
print(f"Pasting at index [{self.sheets.currentIndex()}] cell ({e[0]+r}, {e[1]+c})")
textCell = QTableWidgetItem(); textCell.setText(f'{d}')
self.sheets_tracker[self.sheets.currentIndex()].setItem(e[0]+r, e[1]+c, textCell)
if strategy == self.STRATEGY[1]:
for index, e in enumerate(paste_list):
_, _, d = self.copy_list[index]
d = '' if d is None else d
print(f"Pasting at index [{self.sheets.currentIndex()}] cell ({e[0]}, {e[1]})")
textCell = QTableWidgetItem(); textCell.setText(f'{d}')
self.sheets_tracker[self.sheets.currentIndex()].setItem(e[0], e[1], textCell)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
I'm trying to make an app that shows all the connected files of a certain main file. When you click on a main file, it will show you the list of all files that needs it. As I've started making the app, I'm stuck here one whole day realizing how to create a QFileSystemModel() that only shows the files in a certain list because all the connected files from the mail file are stored in a list.
Here is an example, I want to just show the files in the list which are:
main_file1 = ["[053ALO] - test file.txt", "[053ALO] - test file.txt", "[053ALO] - test file.txt"]
As I've seen on the other related questions, They mentioned the use of QAbstractItemView but I have no Idea how. I'm thinking of iterating the list and making QAbstractItemView in each item and appending it to the treeview but it doesn't show the icon and details of every file which is shown in the picture.
My Question: Is it possible to create a QFileSystemModel() that only shows the files in a certain list?
My Testing Code: (My plan is to use the left side for the main files, and the right one for the connected files)
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QFileSystemModel, \
QHBoxLayout
class FileSystemView(QWidget):
def __init__(self):
super().__init__()
appWidth = 800
appHeight = 300
self.setWindowTitle('File System Viewer')
self.setGeometry(300, 300, appWidth, appHeight)
dir_path = r'<Your directory>'
self.model = QFileSystemModel()
self.model.setRootPath(dir_path)
self.tree = QTreeView()
self.tree.setModel(self.model)
self.tree.setRootIndex(self.model.index(dir_path))
self.tree.setColumnWidth(0, 250)
self.tree.setAlternatingRowColors(True)
self.model2 = QFileSystemModel()
self.model2.setRootPath(dir_path)
self.tree2 = QTreeView()
self.tree2.setModel(self.model2)
self.tree2.setRootIndex(self.model2.index(dir_path))
self.tree2.setColumnWidth(0, 250)
self.tree2.setAlternatingRowColors(True)
layout = QHBoxLayout()
layout.addWidget(self.tree)
layout.addWidget(self.tree2)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())
Update:
With the help of #musicamante I'ved realized that I needed to subclass QSortFilterProxyModel in order to customize the filtering that I want to happen. I'm pretty sure that my approach in the code below is close now but I'm still stuck with this problem where when I clicked on a file in the left side, the similar file in the right side disappears. (seen on this video link)
This is the complete oposite of what I want to happen. What I want to happen is when I click a file on the left side, ONLY the file with the same name will aslo appear on the right side.
I tried changing the condition in the if else statement inside the filterAcceptsRow but it just leaves the right side completely empty.
I've provided the Testing code below:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QFileSystemModel, QHBoxLayout
from PyQt5.QtCore import QSortFilterProxyModel, Qt
class modifiedQSortFilterProxyModel(QSortFilterProxyModel):
def __init__(self):
super().__init__()
self.file = ''
def filterAcceptsRow(self, source_row, source_parent):
filename = self.sourceModel().index(source_row, 0, source_parent).data()
if filename == self.file:
return False
else:
return True
class FileSystemView(QWidget):
def __init__(self):
super().__init__()
appWidth = 800
appHeight = 300
self.setWindowTitle('File System Viewer')
self.setGeometry(300, 300, appWidth, appHeight)
dir_path = r''
# -- left -- #
self.model = QFileSystemModel()
self.model.setRootPath(dir_path)
self.tree = QTreeView()
self.tree.setModel(self.model)
self.tree.setRootIndex(self.model.index(dir_path))
self.tree.setColumnWidth(0, 250)
self.tree.setAlternatingRowColors(True)
self.tree.clicked.connect(self.onClicked)
# -- right -- #
self.model2 = QFileSystemModel()
self.model2.setRootPath(dir_path)
self.filter_proxy_model = modifiedQSortFilterProxyModel()
self.filter_proxy_model.setSourceModel(self.model2)
self.filter_proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
self.filter_proxy_model.setDynamicSortFilter(True)
self.filter_proxy_model.setFilterKeyColumn(0)
root_index = self.model2.index(dir_path)
proxy_index = self.filter_proxy_model.mapFromSource(root_index)
self.tree2 = QTreeView()
self.tree2.setModel(self.filter_proxy_model)
self.tree2.setRootIndex(proxy_index)
self.tree2.setColumnWidth(0, 250)
self.tree2.setAlternatingRowColors(True)
# -- layout -- #
layout = QHBoxLayout()
layout.addWidget(self.tree)
layout.addWidget(self.tree2)
self.setLayout(layout)
def onClicked(self, index):
path = self.sender().model().fileName(index)
self.filter_proxy_model.file = path
self.filter_proxy_model.invalidateFilter()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = FileSystemView()
demo.show()
sys.exit(app.exec_())
There are two problems.
First of all, if you want to show only the index that matches the selected file, you should return True, not False (the function says if the row is accepted, meaning that it's shown).
Then, you cannot just base the filter on the file name: models are hierarchical, and the filter must also match parent indexes (the parent directory); the reason for which you see a blank view is that returning False from a non matching file name, you're filtering out the parent directory.
Considering this, the filter must always accept an index if the parent is different, and eventually return the result of the comparison. To achieve so, you can use the QFileInfo that refers to the source index, which is returned by the models' fileInfo():
class ModifiedQSortFilterProxyModel(QSortFilterProxyModel):
fileInfo = None
def filterAcceptsRow(self, source_row, source_parent):
if not self.fileInfo:
return True
source_index = self.sourceModel().index(source_row, 0, source_parent)
info = self.sourceModel().fileInfo(source_index)
if self.fileInfo.absolutePath() != info.absolutePath():
return True
return self.fileInfo.fileName() == info.fileName()
def setFilter(self, info):
self.fileInfo = info
self.invalidateFilter()
class FileSystemView(QWidget):
# ...
def onClicked(self, index):
self.filter_proxy_model.setFilter(self.model.fileInfo(index))
Note: names of classes and constants should always start with an uppercase letter.
I have a QMainWindow, inside there is a QMenu, QLineEdit, and one QPushButton.
Every time I click the button, it plays a sound and then adds a text to the QLineEdit.
In my QMenu the user must be able to choose which sound plays by checking it.
I tried to achieve this by changing a variable self.s inside the MainWindow class every time a QAction is checked, meanwhile, the other QAction's are unchecked. So in my playsound() I just put the self.view.s as the argument.
But it seems that it's only reading the original self.view.s, which is the first sound. My signals to change self.view.s does not work. Also, the other QActions aren't unchecked as I wanted them to.
Below is my code:
import sys
from functools import partial
from playsound import playsound
from threading import Thread
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.buttons = {}
self.setWindowTitle("Try")
central_widget = QWidget()
self.setCentralWidget(central_widget)
self.lay = QVBoxLayout(central_widget)
self.lineedit()
button = {"HEY! ": (0, 0, 0, 0)}
page = QWidget()
layout = QGridLayout(page)
for btnText, pos in button.items():
self.buttons[btnText] = QPushButton(btnText)
layout.addWidget(self.buttons[btnText], *pos)
self.lay.addWidget(page)
self.music()
def music(self):
self.s = 'sound1.mp3'
self.x = 'sound1.mp3'
self.y = 'sound2.mp3'
self.z = 'disable.mp3'
def lineedit(self):
self.le = QLineEdit()
self.le.setFixedHeight(35)
self.lay.addWidget(self.le)
def set_lineedit(self, text):
self.le.setText(text)
self.le.setFocus()
def line(self):
return self.le.text()
class Menu:
def __init__(self, MainWindow):
super().__init__()
self.view = MainWindow
self.menuBar()
#self.actionSignals()
def menuBar(self):
self.menuBar = QMenuBar()
self.view.setMenuBar(self.menuBar)
self.menu = QMenu(self.menuBar)
self.menu.setTitle('Menu')
self.sounds = QMenu(self.menu)
self.sounds.setTitle('Select Sound')
self.sound1 = QAction(self.menuBar)
self.sound2 = QAction(self.menuBar)
self.disable = QAction(self.menuBar)
self.mute = QAction(self.menuBar)
self.mute.setText('Mute Background')
self.mute.setCheckable(True)
self.mute.setChecked(False)
self.sound1.setText('Sound 1')
self.sound1.setCheckable(True)
self.sound1.setChecked(True)
self.sound2.setText('Sound 2')
self.sound2.setCheckable(True)
self.sound2.setChecked(False)
self.disable.setText('Disable Sound')
self.disable.setCheckable(True)
self.disable.setChecked(False)
self.sounds.addAction(self.sound1)
self.sounds.addAction(self.sound2)
self.sounds.addAction(self.disable)
self.menuBar.addAction(self.menu.menuAction())
self.menu.addAction(self.mute)
self.menu.addAction(self.sounds.menuAction())
def menu_signals(self):
self.sound1.triggered.connect(self.sound_1)
self.sound2.triggered.connect(self.sound_2)
self.disable.triggered.connect(self.disabled)
def sound_1(self, checked):
if checked:
self.sound2.setChecked(False)
self.disable.setChecked(False)
self.view.s = self.view.x
else:
self.sound1.setChecked(True)
def sound_2(self, checked):
if checked:
self.sound1.setChecked(False)
self.disable.setChecked(False)
self.view.s = self.view.y
else:
self.sound2.setChecked(True)
def disabled(self, checked):
if checked:
self.sound2.setChecked(False)
self.sound1.setChecked(False)
self.view.s = self.view.z
else:
self.sound1.setChecked(True)
class Controller:
def __init__(self, MainWindow):
self.view = MainWindow
self.connectSignals()
def background(self):
while True:
playsound('background.mp3')
def playsound(self):
playsound(self.view.s, False)
def buildExpression(self, sub_exp):
expression = self.view.line() + sub_exp
self.view.set_lineedit(expression)
def connectSignals(self):
for btnText, btn in self.view.buttons.items():
self.view.buttons[btnText].clicked.connect(self.playsound)
self.view.buttons[btnText].clicked.connect(partial(self.buildExpression, btnText))
app = QApplication(sys.argv)
w = MainWindow()
x = Controller(w)
Thread(target = x.background, daemon = True).start()
m = Menu(w)
w.show()
app.exec()
I want to be able to change the value within playsound() depending on which QAction is checked in the Menu Bar. While one QAction is checked, the other QAction's should be unchecked.
This is where an action group comes into play. QActionGroup allows for mutually exclusive actions. It also provides convenient access to the selected action through the checkedAction method.
Create a QActionGroup object (e.g. self.soundGroup = QActionGroup(self))
Create your actions with the group as parent (e.g. self.sound1 = QAction(self.soundGroup))
For each of your actions, set their corresponding sound as their data, e.g. self.sound1.setData('sound1.mp3')
Ensure the action group is exclusive (I believe it's the default, but you may use self.soundGroup.setExclusive(True))
Use self.soundGroup.checkedAction() to get the checked action (selected sound) instead of self.view.s: playsound(self.soundGroup.checkedAction().data(), False)
You do not need any of your wiring between the actions and updates to self.view.s anymore. Just remove all of that.
So, I have this simple PyQt5 code which is essentially a file explorer. I need to be able to select arbitrary files or groups of files (directories and all of the children). I would like to:
Add a Checkbox next to each item
If an item is checked/uncheck and has sub-items, the state of the sub-items should be set to the state of the item. So if you check a directory, everything underneath it should also get checked.
When an items check state is changed, directly or indirectly, I need to invoke a callback with the full path (relative to the root) of the item.
I am essentially building a list of selected files to process.
import sys
from PyQt5.QtWidgets import QApplication, QFileSystemModel, QTreeView, QWidget, QVBoxLayout
from PyQt5.QtGui import QIcon
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'PyQt5 file system view - pythonspot.com'
self.left = 10
self.top = 10
self.width = 640
self.height = 480
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.model = QFileSystemModel()
self.model.setRootPath('')
self.tree = QTreeView()
self.tree.setModel(self.model)
self.tree.setAnimated(False)
self.tree.setIndentation(20)
self.tree.setSortingEnabled(True)
self.tree.setWindowTitle("Dir View")
self.tree.resize(640, 480)
windowLayout = QVBoxLayout()
windowLayout.addWidget(self.tree)
self.setLayout(windowLayout)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
The QFileSystemModel doesn't load the contents of a directory until explicitly requested (in case of a tree view, it onyl happens when the directory is expanded the first time).
This requires to carefully verify and set the check state of each path recursively not only whenever a new file or directory is added (or renamed/removed), but also when the directory contents are actually loaded.
In order to correctly implement this, the check states should also be stored using file paths, because when the contents of a directory change some indexes might be invalidated.
The following implementation should take care of all written above, and emit a signal only when an item state is actively changed and the parent state is changed, but not for the children items of a checked directory.
While this choice might seem partially incoherent, it's a performance requirement, as you cannot get the individual signals for each subdirectory (nor you might want to): if you check the top level directory, you might receive thousands of unwanted notifications; on the other hand, it might be important to receive a notification if the parent directory state has changed, whenever all items become checked or unchecked.
from PyQt5 import QtCore, QtWidgets
class CheckableFileSystemModel(QtWidgets.QFileSystemModel):
checkStateChanged = QtCore.pyqtSignal(str, bool)
def __init__(self):
super().__init__()
self.checkStates = {}
self.rowsInserted.connect(self.checkAdded)
self.rowsRemoved.connect(self.checkParent)
self.rowsAboutToBeRemoved.connect(self.checkRemoved)
def checkState(self, index):
return self.checkStates.get(self.filePath(index), QtCore.Qt.Unchecked)
def setCheckState(self, index, state, emitStateChange=True):
path = self.filePath(index)
if self.checkStates.get(path) == state:
return
self.checkStates[path] = state
if emitStateChange:
self.checkStateChanged.emit(path, bool(state))
def checkAdded(self, parent, first, last):
# if a file/directory is added, ensure it follows the parent state as long
# as the parent is already tracked; note that this happens also when
# expanding a directory that has not been previously loaded
if not parent.isValid():
return
if self.filePath(parent) in self.checkStates:
state = self.checkState(parent)
for row in range(first, last + 1):
index = self.index(row, 0, parent)
path = self.filePath(index)
if path not in self.checkStates:
self.checkStates[path] = state
self.checkParent(parent)
def checkRemoved(self, parent, first, last):
# remove items from the internal dictionary when a file is deleted;
# note that this *has* to happen *before* the model actually updates,
# that's the reason this function is connected to rowsAboutToBeRemoved
for row in range(first, last + 1):
path = self.filePath(self.index(row, 0, parent))
if path in self.checkStates:
self.checkStates.pop(path)
def checkParent(self, parent):
# verify the state of the parent according to the children states
if not parent.isValid():
return
childStates = [self.checkState(self.index(r, 0, parent)) for r in range(self.rowCount(parent))]
newState = QtCore.Qt.Checked if all(childStates) else QtCore.Qt.Unchecked
oldState = self.checkState(parent)
if newState != oldState:
self.setCheckState(parent, newState)
self.dataChanged.emit(parent, parent)
self.checkParent(parent.parent())
def flags(self, index):
return super().flags(index) | QtCore.Qt.ItemIsUserCheckable
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.CheckStateRole and index.column() == 0:
return self.checkState(index)
return super().data(index, role)
def setData(self, index, value, role, checkParent=True, emitStateChange=True):
if role == QtCore.Qt.CheckStateRole and index.column() == 0:
self.setCheckState(index, value, emitStateChange)
for row in range(self.rowCount(index)):
# set the data for the children, but do not emit the state change,
# and don't check the parent state (to avoid recursion)
self.setData(index.child(row, 0), value, QtCore.Qt.CheckStateRole,
checkParent=False, emitStateChange=False)
self.dataChanged.emit(index, index)
if checkParent:
self.checkParent(index.parent())
return True
return super().setData(index, value, role)
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.tree = QtWidgets.QTreeView()
layout.addWidget(self.tree, stretch=2)
model = CheckableFileSystemModel()
model.setRootPath('')
self.tree.setModel(model)
self.tree.setSortingEnabled(True)
self.tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.logger = QtWidgets.QPlainTextEdit()
layout.addWidget(self.logger, stretch=1)
self.logger.setReadOnly(True)
model.checkStateChanged.connect(self.updateLog)
self.resize(640, 480)
QtCore.QTimer.singleShot(0, lambda: self.tree.expand(model.index(0, 0)))
def updateLog(self, path, checked):
if checked:
text = 'Path "{}" has been checked'
else:
text = 'Path "{}" has been unchecked'
self.logger.appendPlainText(text.format(path))
self.logger.verticalScrollBar().setValue(
self.logger.verticalScrollBar().maximum())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
I would like to know how to edit only one column of a QTreeWidgetItem.
Currently, I can edit all columns of a QTreeWidgetItem.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import sys
app = QApplication(sys.argv)
qApp = app
pointListBox = QTreeWidget()
header=QTreeWidgetItem(["Tree","First","secondo"])
pointListBox.setHeaderItem(header)
root = QTreeWidgetItem(pointListBox, ["root"])
A = QTreeWidgetItem(root, ["A"])
barA = QTreeWidgetItem(A, ["bar", "i", "ii"])
bazA = QTreeWidgetItem(A, ["baz", "a", "b"])
barA.setFlags(barA.flags() | Qt.ItemIsEditable)
bazA.setFlags(bazA.flags() | Qt.ItemIsEditable)
pointListBox.show()
sys.exit(app.exec_())
EDIT
I have added some changes but I don't know how to edit the second column of my QtreeWidgetItem.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow,self).__init__(parent)
self.initUI()
def createGroup(self):
groupBox = QGroupBox()
self.treeWidget = QTreeWidget()
header=QTreeWidgetItem(["Tree","First","secondo"])
#...
self.treeWidget.setHeaderItem(header) #Another alternative is setHeaderLabels(["Tree","First",...])
root = QTreeWidgetItem(self.treeWidget, ["root"])
A = QTreeWidgetItem(root, ["A"])
barA = QTreeWidgetItem(A, ["bar", "i", "ii"])
bazA = QTreeWidgetItem(A, ["baz", "a", "b"])
barA.setFlags(barA.flags() | Qt.ItemIsEditable)
bazA.setFlags(bazA.flags() | Qt.ItemIsEditable)
# switch off "default" editing behaviour
# as it does not allow to configure only an individual
# column as editable
self.treeWidget.setEditTriggers(self.treeWidget.NoEditTriggers)
# to be able to decide on your own whether a particular item
# can be edited, connect e.g. to itemDoubleClicked
self.treeWidget.itemDoubleClicked.connect(self.checkEdit)
vbox = QVBoxLayout()
vbox.addWidget(self.treeWidget)
vbox.addStretch(1)
groupBox.setLayout(vbox)
return groupBox
# in your connected slot, you can implement any edit-or-not-logic
# you want
def checkEdit(self, item, column):
# e.g. to allow editing only of column 1:
if column == 1:
self.treeWidget.editItem(item[1])
def initUI(self):
self.resize(300, 220)
self.grid = QGridLayout()
self.widget = QWidget()
self.widget.setLayout(self.grid)
self.setCentralWidget(self.widget)
self.grid.addWidget(self.createGroup(),1,0,1,2)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle(QStyleFactory.create("Fusion"))
form = MainWindow()
form.show()
sys.exit(app.exec_())
Try this, it's work for me.
self.treeWidget.itemDoubleClicked.connect(self.checkEdit)
def checkEdit(self, item, column):
tmp = item.flags()
if column == 1 :
item.setFlags(tmp | QtCore.Qt.ItemIsEditable)
elif tmp & QtCore.Qt.ItemIsEditable:
item.setFlags(tmp ^ QtCore.Qt.ItemIsEditable)
There are no methods / flags to configure this directly.
You can solve this by switching off Qt's EditTriggers completely and implementing your own function that decides whether an item should be edited or not:
class MyWidget(QWidget):
def __init__(parent):
self.treeWidget = ...
# switch off "default" editing behaviour
# as it does not allow to configure only an individual
# column as editable
self.treeWidget.setEditTriggers(self._treeWidget.NoEditTriggers)
# to be able to decide on your own whether a particular item
# can be edited, connect e.g. to itemDoubleClicked
self.treeWidget.itemDoubleClicked.connect(self.checkEdit)
# in your connected slot, you can implement any edit-or-not-logic
# you want
def checkEdit(self, item, column):
# e.g. to allow editing only of column 1:
if column == 1:
self.treeWidget.editItem(item, column)
If you'd like to editing on other occasions as well, simply connect checkEdit to the according signals, such as itemClicked, itemEntered etc.