Data will not save when using ComboBoxes inside QTableWidget - python

import sys
from PyQt6 import QtWidgets
from PyQt6.QtWidgets import QApplication, QItemDelegate, QWidget, QTableWidget, QTableWidgetItem, QPushButton, QHeaderView, QHBoxLayout, QVBoxLayout
from PyQt6.QtCore import Qt
import pandas as pd
class ComboBoxDelegate(QItemDelegate):
def __init__(self, parent=None):
super(ComboBoxDelegate, self).__init__(parent)
self.items = []
def setItems(self, items):
self.items = items
def createEditor(self, widget, option, index):
editor = QtWidgets.QComboBox(widget)
editor.addItems(self.items)
return editor
def setEditorData(self, editor, index):
value = index.model().data(index, Qt.ItemDataRole.EditRole)
if value:
editor.setCurrentText(str(value))
print("Data Changed")
def setModelData(self, editor, model, index):
model.setData(index, editor.currentIndex(), Qt.ItemDataRole.EditRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class MyApp(QWidget):
def __init__(self):
super().__init__()
self.window_width, self.window_height = 700, 500
self.resize(self.window_width, self.window_height)
self.setWindowTitle('Load Excel (or CSV) data to QTableWidget')
layout = QVBoxLayout()
self.setLayout(layout)
self.table = QTableWidget()
layout.addWidget(self.table)
self.button = QPushButton('&Load Data')
self.button.clicked.connect(lambda _, xl_path=excel_file_path,: self.loadExcelData(xl_path))
layout.addWidget(self.button)
def loadExcelData(self, excel_file_dir):
df = pd.read_csv(excel_file_dir)
if df.size == 0:
return
df.fillna('', inplace=True)
self.table.setRowCount(df.shape[0])
self.table.setColumnCount(df.shape[1])
self.table.setHorizontalHeaderLabels(df.columns)
# returns pandas array object
for row in df.iterrows():
values = row[1]
for col_index, value in enumerate(values):
if isinstance(value, (float, int)):
value = '{0:0,.0f}'.format(value)
tableItem = QTableWidgetItem(str(value))
self.table.setItem(row[0], col_index, tableItem)
self.create_delegate(self.table)
self.table.setColumnWidth(2, 300)
def create_delegate(self, table):
test_del = ComboBoxDelegate()
test_list = ["Gravity Main", "Manhole", "Lateral"]
test_del.setItems(test_list)
table.setItemDelegateForColumn(0, test_del)
if __name__ == '__main__':
excel_file_path = "[Your CSV]"
app = QApplication(sys.argv)
app.setStyleSheet('''
QWidget {
font-size: 17px;
}
''')
myApp = MyApp()
myApp.show()
try:
sys.exit(app.exec())
except SystemExit:
print('Closing Window...')
Extension to previous question
Change the file directory to a random csv.
This populates the QTableWidget and does the same thing as my main program.
The QTableWidget does not save the changes.
My goal is to be able to populate multiple columns with different options based on the csv that is loaded. I had a working version but using QTableView instead of QTableWidget and I am working on transferring that progress.

The issue is caused by a code generation bug, according to the PyQt maintainer.
It should have been fixed in the following PyQt6 snapshots, so it's very likely that if you do a pip update you'll get the working version already, otherwise wait a couple of days and keep an eye on the changelog of the official site.

Related

Can QCompleter's list of values be modified/updated later again in pyqt5?

I am making a GUI using Pyqt5 where there are 5 Qlineedit fields.
The range of values for each can be say [1 -5]. If the user tried to select third Qlineedit field and selected the value '2'. The other fields range should now not include '2' in it.
Similarly if he selected another field with value '4', the remaining fields should have only [1, 3, 5] as available options.
(Please bear in mind that user can delete the value in any field too and available values should be updated accordingly.)
I tried to use list and update QCompleter for the fields as soon as I see any textChanged signal.
The below code works perfectly except when the user lets say had given the input '14' in some field before and now tries to delete it using backspace. '4' will get deleted but on deleting '1' it will crash without any backtrace.
"Process finished with exit code -1073741819 (0xC0000005)"
If I click on the lineedit field before deleting '1', it works fine.
Minimal code -
#!/usr/bin/env python
import logging
import sys
import traceback
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QApplication, QCompleter
class MyWidget(QWidget):
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
vbox = QVBoxLayout(self)
self.setLayout(vbox)
self.names = ['11', '12', '13', '14', '15']
self.names.sort()
self.line_edits_list = [0]*5
for i in range(len(self.names)):
self.line_edits_list[i] = QLineEdit(self)
completer = QCompleter(self.names, self.line_edits_list[i])
self.line_edits_list[i].setCompleter(completer)
vbox.addWidget(self.line_edits_list[i])
self.line_edits_list[i].textEdited.connect(self.text_changed)
def text_changed(self, text):
names_sel = []
# Check if current text matches anything in our list, if it does add it to a new list
for i in range(len(self.line_edits_list)):
if self.line_edits_list[i].text() in self.names and self.line_edits_list[i].text() not in names_sel:
names_sel.append(self.line_edits_list[i].text())
# The remaining textfields should get their qcompleter ranges updated with unique values of the two lists
for i in range(len(self.line_edits_list)):
if self.line_edits_list[i].text() not in self.names:
try:
new_range = list((set(self.names) - set(names_sel)))
completer = QCompleter(new_range, self.line_edits_list[i])
self.line_edits_list[i].setCompleter(completer)
except:
print(traceback.format_exc())
def test():
app = QApplication(sys.argv)
w = MyWidget()
w.show()
app.exec_()
print("END")
if __name__ == '__main__':
test()
As I pointed out in the comments, using QLineEdit is not a suitable widget if you want to restrict the values that the user can select. In this case, as the user has multiple options, then a suitable widget is the QComboBox. To do the filtering you can use QSortFilterProxyModel overriding the filterAcceptsRow method to implement custom logic.
#!/usr/bin/env python
import sys
from PyQt5.QtCore import QSortFilterProxyModel
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QApplication, QComboBox, QVBoxLayout, QWidget
class ProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._hide_items = []
#property
def hide_items(self):
return self._hide_items
#hide_items.setter
def hide_items(self, items):
self._hide_items = items
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent):
index = self.sourceModel().index(sourceRow, 0, sourceParent)
return index.data() not in self.hide_items
class MyWidget(QWidget):
def __init__(self, names, parent=None):
super(MyWidget, self).__init__(parent)
self._comboboxes = []
model = QStandardItemModel()
vbox = QVBoxLayout(self)
for name in names:
model.appendRow(QStandardItem(name))
proxy_model = ProxyModel()
proxy_model.setSourceModel(model)
combobox = QComboBox()
combobox.setModel(proxy_model)
vbox.addWidget(combobox)
combobox.setCurrentIndex(-1)
combobox.currentIndexChanged.connect(self.handle_currentIndexChanged)
self.comboboxes.append(combobox)
self.resize(640, 480)
#property
def comboboxes(self):
return self._comboboxes
def handle_currentIndexChanged(self):
rows = []
for combobox in self.comboboxes:
if combobox.currentIndex() != -1:
rows.append(combobox.currentText())
for i, combobox in enumerate(self.comboboxes):
index = combobox.currentIndex()
proxy_model = combobox.model()
r = rows[:]
if index != -1:
text = combobox.currentText()
if text in r:
r.remove(text)
combobox.blockSignals(True)
proxy_model.hide_items = r
combobox.blockSignals(False)
def test():
app = QApplication(sys.argv)
names = ["11", "12", "13", "14", "15"]
w = MyWidget(names)
w.show()
app.exec_()
if __name__ == "__main__":
test()
The correct way to implement this is by using a model with the QCompleter. The model can change its content over time and the completer will react to it accordingly.
In your case, you could create a model that contains all possible values first. Then, you could use a QSortFilterProxyModel, which, given you current UI state, could reduce the set. The proxy model is the one that you use with QCompleter.
This (otherwise unrelated) question is example Python code for implementing such a proxy model: QSortFilterProxyModel does not apply Caseinsensitive

How to customize Qtreewidget item editor in PyQt5?

I am making a QtreeWidget with item editable,but the problem is with the Item Editor or QAbstractItemDelegate(might be called like this,not sure).I am unable to change the stylesheet,actually i dont know how to do this.And also i want the selected lines(blue in editor) should be according to my wish.like below picture
here i want that blue selected line upto ".jpg",so that anyone cant change that ".jpg". Only ,one can change upto this".jpg"
Here is my code:
import sys
from PyQt5 import QtCore, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.button = QtWidgets.QPushButton('Edit')
self.button.clicked.connect(self.edittreeitem)
self.tree = QtWidgets.QTreeWidget()
self.tree.setStyleSheet('background:#333333;color:grey')
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.tree)
layout.addWidget(self.button)
columns = 'ABCDE'
self.tree.setColumnCount(len(columns))
for index in range(50):
item=QtWidgets.QTreeWidgetItem(
self.tree, [f'{char}{index:02}.jpg' for char in columns])
item.setFlags(item.flags()|QtCore.Qt.ItemIsEditable)
def edittreeitem(self):
getSelected = self.tree.selectedItems()
self.tree.editItem(getSelected[0],0)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setWindowTitle('Test')
window.setGeometry(800, 100, 540, 300)
window.show()
sys.exit(app.exec_())
You can create your own delegate that only considers the base name without the extension, and then set the data using the existing extension.
class BaseNameDelegate(QtWidgets.QStyledItemDelegate):
def setEditorData(self, editor, index):
editor.setText(QtCore.QFileInfo(index.data()).completeBaseName())
def setModelData(self, editor, model, index):
name = editor.text()
if not name:
return
suffix = QtCore.QFileInfo(index.data()).suffix()
model.setData(index, '{}.{}'.format(name, suffix))
class Window(QtWidgets.QWidget):
def __init__(self):
# ...
self.tree.setItemDelegate(BaseNameDelegate(self.tree))
The only drawback of this is that the extension is not visible during editing, but that would require an implementation that is a bit more complex than that, as QLineEdit (the default editor for string values of a delegate) doesn't provide such behavior.

python GUI autocomplete by key value

I am trying to create an autocomplete GUI in python such that as I type a first name, I see possible last names. For example, let's say I have this dictionary: {"George": ["Washington", "Bush"]}. When I start typing "G", I want it to show "Washington" and "Bush". When "Washington" is selected, I want "Washington" to show. I am new to GUIs and I think PyQt has an example of autocompletion, but the words are not in key value pairs but a list of words.
https://wiki.python.org/moin/PyQt/Adding%20auto-completion%20to%20a%20QLineEdit
Is there a way to edit the code in the link so that I can enable this feature? Thank you!
You have to override the pathFromIndex method so that when you select some text, the appropriate option is written in the QLineEdit, and to change what is shown in the popup a delegate should be used.
from PyQt5 import QtCore, QtGui, QtWidgets
def create_model(d):
model = QtGui.QStandardItemModel()
for key, value in d.items():
for val in value:
it = QtGui.QStandardItem(key)
it.setData(val, QtCore.Qt.UserRole)
model.appendRow(it)
return model
class StyledItemDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super(StyledItemDelegate, self).initStyleOption(option, index)
option.text = index.data(QtCore.Qt.UserRole)
class Completer(QtWidgets.QCompleter):
def __init__(self, parent=None):
super(Completer, self).__init__(parent)
QtCore.QTimer.singleShot(0, self.change_delegate)
#QtCore.pyqtSlot()
def change_delegate(self):
delegate = StyledItemDelegate(self)
self.popup().setItemDelegate(delegate)
def pathFromIndex(self, index):
return index.data(QtCore.Qt.UserRole)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
d = {
"George": ["Washington", "Bush"],
"Languages": ["Python", "C++"]
}
model = create_model(d)
w = QtWidgets.QLineEdit()
completer = Completer(w)
completer.setModel(model)
w.setCompleter(completer)
w.show()
sys.exit(app.exec_())

Update PyQt menu

I am trying to dynamically update a menu with new items when I add new items via the form into Qsettings. For example, if you open my code and click the button and then click "new" it will open a QLineEdit with a button. When the button is clicked the list data gets stored via Qsettings.
I'd like to be able to update the menu somehow to show the items without restarting. I tried some things like calling repaint and update in a few areas with no luck.
Here is my code, I slimmed it down the best that I could for the example.
import functools
import sys
from PyQt5 import QtCore
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, \
QVBoxLayout, QLineEdit,QApplication, QWidgetAction, QTextBrowser, QAction, QMenu
class MainWindow(QWidget):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.layout = QHBoxLayout()
self.menu_action = QAction(QIcon("icon.png"),"New", self)
self.menu_btn = QPushButton()
self.menu = MyMenu("Menu", self.menu_btn)
self.add_menu = self.menu.addMenu(QIcon("icon.png"), "Menu")
self.add_menu.addAction(self.menu_action)
self.menu_btn.setMenu(self.menu)
self.textBox = QTextBrowser(self)
action = QWidgetAction(self.menu_btn)
action.setDefaultWidget(self.textBox)
self.menu_btn.menu().addAction(action)
settings = QtCore.QSettings('test_org', 'my_app')
self.new_items = settings.value('new_item', [])
print('%s' % self.new_items)
for item in self.new_items:
self.create_action = QAction(QIcon("icon.png"), item[0], self)
self.create_action.setData(item)
self.add_menu.addAction(self.create_action)
self.create_action.triggered.connect(functools.partial(self.menu_clicked, self.create_action))
self.layout.addWidget(self.menu_btn)
self.setLayout(self.layout)
self.menu_action.triggered.connect(self.open_window)
def open_window(self):
self.create_menu_item = Create_Menu_Item()
self.create_menu_item.show()
def menu_clicked(self, item):
itmData = item.data()
print(itmData)
class Create_Menu_Item(QWidget):
def __init__(self, parent=None):
super(Create_Menu_Item, self).__init__(parent)
self.resize(200, 195)
self.layout = QVBoxLayout()
self.form = QLineEdit()
self.btn = QPushButton()
self.layout.addWidget(self.form)
self.layout.addWidget(self.btn)
self.btn.clicked.connect(self.save_new_item)
self.setLayout(self.layout)
def save_new_item(self):
settings = QtCore.QSettings('test_org', 'my_app')
self.new_item = settings.value('new_item', [])
self.new_item.append([self.form.text()])
settings.setValue('new_item', self.new_item)
print(self.new_item)
print('saved!')
class MyMenu(QMenu):
def event(self,event):
if event.type() == QtCore.QEvent.Show:
self.move(self.parent().mapToGlobal(QtCore.QPoint(0,0))-QtCore.QPoint(0,self.height()))
return super(MyMenu,self).event(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
Anyone have any ideas? Thanks.
You can create a for loop to access the list.
Bare in mind it is a list of lists so a nested for loop is neccessary
def save_new_item(self):
settings = QtCore.QSettings('test_org', 'my_app')
self.new_item = settings.value('new_item', [])
self.new_item.append([self.form.text()])
settings.setValue('new_item', self.new_item)
print(self.new_item)
print('saved!')
# Add new menu items..
for x in self.new_item:
for y in x:
print(y)
The above will keep adding the WHOLE list of items every time you add a new item..
To just add the newest item, this is all you need (the last item added to the list)
w.add_menu.addAction(self.new_item[0][-1])
so
def save_new_item(self):
settings = QtCore.QSettings('test_org', 'my_app')
self.new_item = settings.value('new_item', [])
self.new_item.append([self.form.text()])
settings.setValue('new_item', self.new_item)
print(self.new_item)
print('saved!')
#ADD last item in the list
w.add_menu.addAction(self.new_item[0][-1])

Populating a table in PyQt with file attributes

I'm trying to populate a QTableWidget with the attributes of a file opened elsewhere in the script. I've successfully set the file attributes read in from the file using the following code:
class FileHeader(object):
fileheader_fields=(
"filetype","fileversion","numframes",
"framerate","resolution","numbeams",
"samplerate","samplesperchannel","receivergain",
"windowstart","winlengthsindex","reverse",
"serialnumber","date","idstring","ID1","ID2",
"ID3","ID4","framestart","frameend","timelapse",
"recordInterval","radioseconds","frameinterval","userassigned")
fileheader_formats=(
'S3','B','i4','i4','i4','i4','f','i4','i4','i4',
'i4','i4','i4','S32','S256','i4','i4','i4','i4',
'i4','i4','i4','i4','i4','i4','S136')
IMAGE_HEADER_BYTES = 256
IMAGES_DATA_BYTES = 49152
def __init__(self,filename=''):
self.filename = filename
if filename:
self.setFile(filename)
else:
# initialize our attributes to None
for field in self.fileheader_fields:
setattr(self,field,None)
def setFile(self, f):
self.infile=open(f, 'rb')
dtype=dict(names=self.fileheader_fields, formats=self.fileheader_formats)
self.fileheader=np.fromfile(self.infile, dtype=dtype, count=1)
self.fileheader_length=self.infile.tell()
for field in self.fileheader_fields:
setattr(self,field,self.fileheader[field])
I've used this code to populate the table but I keep getting a "FileHeader has no attribute fileheader" error.
from fileheader import FileHeader, Frame
from echogram import QEchogram
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import os, sys
class MainWindow(QWidget):
def __init__(self, parent=None):
self.fileheader_fields=FileHeader.fileheader_fields
self.fileheader_values=FileHeader.fileheader[field]
self.fileheader={field: "value of" + field
for field in self.fileheader_fields}
super(MainWindow, self).__init__(parent)
self.fileheader_table=QTableWidget()
layout=QVBoxLayout()
layout.addWidget(self.fileheader_table)
self.setLayout(layout)
self.populate
def populate(self):
self.fileheader_table.setRowCount(len(self.fileheader_fields))
self.fileheader_table.sestColumnCount(2)
self.fileheader_table.setHorizontalHeaderLabels(['name','value'])
for i,field in enumerate(self.fileheader_fields):
name=QTableWidgetItem(field)
value=QTableWidgetItem(self.fileheader[field])
self.fileheader_table.setItem(i,0,name)
self.fileheader_table.setItem(i,1,value)
if __name__=="__main__":
app=QApplication(sys.argv)
filename=str(QFileDialog.getOpenFileName(None,"open file","C:/vprice/DIDSON/DIDSON Data","*.ddf"))
wnd=MainWindow()
wnd.resize(640,480)
wnd.show()
#echoGram=QEchogram()
#echoGram.initFromFile(filename)
#fileName="test.png"
#echoGram.processEchogram()
#dataH=echoGram.data
#print "Horizontal data", dataH
Bear with me-- I just started with all of the Python stuff about a month ago...
See populate method. Also there is some examples in documentation
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore, QtGui
class MainWindow(QtGui.QWidget):
def __init__(self, parent=None):
self.fileheader_fields=(
"filetype","fileversion","numframes",
"framerate","resolution","numbeams",
"samplerate","samplesperchannel","receivergain",
"windowstart","winlengthsindex","reverse",
"serialnumber","date","idstring","ID1","ID2",
"ID3","ID4","framestart","frameend","timelapse",
"recordInterval","radioseconds","frameinterval","userassigned"
)
# just for test
self.fileheader = {field: 'value of ' + field
for field in self.fileheader_fields}
super(MainWindow, self).__init__(parent)
self.table_widget = QtGui.QTableWidget()
layout = QtGui.QVBoxLayout()
layout.addWidget(self.table_widget)
self.setLayout(layout)
self.populate()
def populate(self):
self.table_widget.setRowCount(len(self.fileheader_fields))
self.table_widget.setColumnCount(2)
self.table_widget.setHorizontalHeaderLabels(['name', 'value'])
for i, field in enumerate(self.fileheader_fields):
name = QtGui.QTableWidgetItem(field)
value = QtGui.QTableWidgetItem(self.fileheader[field])
self.table_widget.setItem(i, 0, name)
self.table_widget.setItem(i, 1, value)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
wnd = MainWindow()
wnd.resize(640, 480)
wnd.show()
sys.exit(app.exec_())
UPD
Code for your concrete case:
from fileheader import FileHeader, Frame
from echogram import QEchogram
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import os, sys
class MainWindow(QWidget):
def __init__(self, filename, parent=None):
super(MainWindow, self).__init__(parent)
# here we are loading file
# now self.fileheader contains attributes
self.fileheader = FileHeader(filename)
self.fileheader_table = QTableWidget()
layout = QVBoxLayout()
layout.addWidget(self.fileheader_table)
self.setLayout(layout)
self.populate()
def populate(self):
self.fileheader_table.setRowCount(len(self.fileheader.fileheader_fields))
self.fileheader_table.sestColumnCount(2)
self.fileheader_table.setHorizontalHeaderLabels(['name','value'])
for i,field in enumerate(self.fileheader.fileheader_fields):
name=QTableWidgetItem(field)
value=QTableWidgetItem(getattr(self.fileheader, field))
self.fileheader_table.setItem(i,0,name)
self.fileheader_table.setItem(i,1,value)
if __name__=="__main__":
app=QApplication(sys.argv)
filename=str(QFileDialog.getOpenFileName(None,"open file","C:/vprice/DIDSON/DIDSON Data","*.ddf"))
wnd=MainWindow(filename)
wnd.resize(640,480)
wnd.show()
#echoGram=QEchogram()
#echoGram.initFromFile(filename)
#fileName="test.png"
#echoGram.processEchogram()
#dataH=echoGram.data
#print "Horizontal data", dataH
ok u have to add a QTableWidget to your custom QtGui.QMainWindow
i would create i whith 0 line and the right amount of collumn,like this:
self.tableArea=QtGui.QTableWidget(0,self.fileheader_length,self)
self.setHorizontalHeaderLabels(['nameOne','nameTwo'])#will name your top header
self.verticalHeader().setVisible(False)#will hide your left header if you don't need them
and add your line one by one
self.tableArea.insertRow(row)#it will add a line at the row position
after that u have to create a QTableWidgetItem for each cell
itemTo=QtGui.QTableWidgetItem('text')#replace text by your value
and place your item in the cell you want
self.tableArea.setItem(row, column, itemTo)

Categories