So I've been trying to implement a proper TreeView that displays directories and files depending on user input - I am allowing the user to add directories and files to his "project" either recursively or otherwise, after which I create my own tree view of the contents of said project.
Now, my problem is that even though most of the documentation and other questions I've found on this subject seem to want to disable the editability of treeview items, I am trying ( and failing ) to find a way to enable it. What I would like is for the user to be able to double click on any cell in any column of my treeview and then edit its contents. Does anyone know how to do this?
Below is the code I am using to generate a tab in a tabView Widget, after which i then add the TreeView. The items of the TreeView are later added through the AddParent and AddChild methods.
class treeTab(QtWidgets.QWidget):
def __init__(self,core,main,label):
super (treeTab,self).__init__()
self.label = label
self.core = core
self.sizes = core.UISizes
self.tab_sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,QtWidgets.QSizePolicy.Expanding)
self.tree = QtWidgets.QTreeWidget(self)
self.tree.setColumnCount(len(self.sizes.projectTreeColumnLabels))
self.tree.setHeaderLabels(self.sizes.projectTreeColumnLabels)
self.tree.setSizePolicy(self.tab_sizePolicy)
self.tree_layout = QtWidgets.QGridLayout()
self.tree_layout.objectName = self.label + "TreeGridLayout"
self.tree.setLayout(self.tree_layout)
self.treeroot = self.tree.invisibleRootItem()
self.tree.setSelectionMode(Qt.QAbstractItemView.ContiguousSelection)
def addParent(self, parent, column, title, data):
item = QtWidgets.QTreeWidgetItem(parent, [title])
item.setData(column, QtCore.Qt.UserRole, data)
item.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.ShowIndicator)
item.setExpanded (True)
return item
def addChild(self, parent, column, title, data):
item = QtWidgets.QTreeWidgetItem(parent, [title])
item.setData(column, QtCore.Qt.UserRole, data)
item.setText(1,data.print_tags())
item.setText(2,data.category.name)
item.setText(3,data.format)
item.setCheckState (column, QtCore.Qt.Unchecked)
item.setFlags(item.flags() or QtCore.Qt.ItemIsEditable)
return item
You have confused binary operators and Boolean operators. Boolean operators (such as and and or) are used with Boolean values (such as True and False) to produce a single True or False once the expression is evaluated.
However, the flags and not Boolean values. They are integers which are powers of 2 (or binary numbers with only one bit set), such that they can be combined into a single integer that represents whether each flag is enabled or disabled. For instance, 2 is represented in binary as 0b010. 4 is represented as 0b100. If you bitwise or these together, you get 0b110, indicating that both the flag equal to 2 and the flag equal to 4 is set. However the flag equal to 1 is not set (the 0 in the 0b110).
In short, you should set the flags using the bitwise or operator (|):
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
So I managed to figure this one out - it turned out to be quite simple in the end :)
In order to create an 'editable' treeView item where you can double click on the text of a particular item to edit it, you simply need to change the widget contained in that particular column of the item to a QLineEdit Widget that deletes itself upon pressing enter. The code for this looks as follows:
This is the code to connect the double click event to a method that gets the currently selected item and replaces it with a QLineEdit Widget:
self.tree.itemDoubleClicked.connect(self.editItem)
def editItem(self,*args):
itm = self.tree.itemFromIndex(self.tree.selectedIndexes()[0])
column = self.tree.currentColumn()
edit = QtWidgets.QLineEdit()
edit.returnPressed.connect(lambda*_:self.project.setData(column,edit.text(),itm,column,self.tree))
edit.returnPressed.connect(lambda*_:self.update())
self.tree.setItemWidget(itm,column,edit)
Note especially the combination of the following code:
itm = self.tree.itemFromIndex(self.tree.selectedIndexes()[0])
column = self.tree.currentColumn()
This code in effect gives you both the row and the column of the currently selected item, which is useful if you want to edit column items separately.
Now You'll be asking yourself why I'm passing so many parameters to the 'setData' method: this is purely for the purposes of my particular project, so don't worry about it. the 'returnPressed' event simply needs to connect to the proper method to handle whatever data it contains, and then delete itself. in my code, this looks like this:
def setData(self,dataTypeIndex,data,item,column,tree):
if dataTypeIndex == 0:
# filename
self.name = data
elif dataTypeIndex == 1:
# tags
data = data.split(",")
self.tags = []
for tag in data:
self.tags.append(Tag(tag))
elif dataTypeIndex == 2:
# category
self.category.name = data
tree.setItemWidget(item,column,None)
This last line of code ( tree.setItemWidget(item,column,None) ) is where the QlineEdit is unparented and therefore effectively removed.
Related
I'm working on a QAction to paste structured text from the clipboard into a QTableWidget. This is my current code:
class PasteCellsAction(qt.QAction):
def __init__(self, table):
if not isinstance(table, qt.QTableWidget):
raise ValueError('CopySelectedCellsAction must be initialised ' +
'with a QTableWidget.')
super(PasteCellsAction, self).__init__(table)
self.table = table
self.setText("Paste")
self.setShortcut(qt.QKeySequence('Ctrl+V'))
self.triggered.connect(self.pasteCellFromClipboard)
def pasteCellFromClipboard(self):
"""Paste text from cipboard into the table.
If the text contains tabulations and
newlines, they are interpreted as column and row separators.
In such a case, the text is split into multiple texts to be paste
into multiple cells.
:return: *True* in case of success, *False* if pasting data failed.
"""
selected_idx = self.table.selectedIndexes()
if len(selected_idx) != 1:
msgBox = qt.QMessageBox(parent=self.table)
msgBox.setText("A single cell must be selected to paste data")
msgBox.exec_()
return False
selected_row = selected_idx[0].row()
selected_col = selected_idx[0].column()
qapp = qt.QApplication.instance()
clipboard_text = qapp.clipboard().text()
table_data = _parseTextAsTable(clipboard_text)
protected_cells = 0
out_of_range_cells = 0
# paste table data into cells, using selected cell as origin
for row in range(len(table_data)):
for col in range(len(table_data[row])):
if selected_row + row >= self.table.rowCount() or\
selected_col + col >= self.table.columnCount():
out_of_range_cells += 1
continue
item = self.table.item(selected_row + row,
selected_col + col)
# ignore empty strings
if table_data[row][col] != "":
if not item.flags() & qt.Qt.ItemIsEditable:
protected_cells += 1
continue
item.setText(table_data[row][col])
if protected_cells or out_of_range_cells:
msgBox = qt.QMessageBox(parent=self.table)
msg = "Some data could not be inserted, "
msg += "due to out-of-range or write-protected cells."
msgBox.setText(msg)
msgBox.exec_()
return False
return True
I want to test if a cell is editable before pasting data in it, and for this I get the item using QTableWidget.item(row, col) and then I checks the flags of the item.
My problem is that the .item method returns None for empty cells, so I can't check the flags of empty cells. My code currently only works when there is no empty cell in the paste area.
The error is in lines 46 (None returned) and 50 (AttributeError: 'NoneType' object has no attribute 'flags'):
item = self.table.item(selected_row + row,
selected_col + col)
# ignore empty strings
if table_data[row][col] != "":
if not item.flags() & qt.Qt.ItemIsEditable:
...
Is there another way of finding out if the cell is editable, other than checking flags of the item?
The dimensions of QTableWidget a can specified without explicitly adding any items. In which case, the cells will be completely empty - i.e. both the data and the item will be None. If the user edits the cell, the data will be added to the table's model, and an item will be added. This will happen even if the entered value is an empty string. By default, all cells will be editable unless you take explicit steps to make them read-only.
There are numerous ways of making cells read-only - for instance, setting the edit triggers, or overriding the tables's edit method. But if your only method is explicitly setting the flags on individual table-widget items, you can safely assume that a cell with no item is both editable and empty. (Note that if you directly set the data via the table's model rather than by using e.g. setItem, the cell will still automatically have an item).
I found a solution that seems to work: create a new item and add it to the table when the item() method returns None.
I still have some doubts abouts whether there is a risk that this might modify the flags of a write-protected cell. I currently just assume that if a cell is write-protected, this means that it necessarily already contains an item.
item = self.table.item(target_row,
target_col)
# item may not exist for empty cells
if item is None:
item = qt.QTableWidgetItem()
self.table.setItem(target_row,
target_col,
item)
# ignore empty strings
if table_data[row_offset][col_offset] != "":
if not item.flags() & qt.Qt.ItemIsEditable:
protected_cells += 1
continue
item.setText(table_data[row_offset][col_offset])
EDIT: target_row = selected_row + row_offset ...
there is the dialog:
class classsearchresult(QDialog, Ui_Dialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent)
self.setupUi(self)
self.tableView.setShowGrid(False)
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
vh = self.tableView.verticalHeader()
vh.setVisible(False)
hh = self.tableView.horizontalHeader()
hh.setVisible(False)
hh.setStretchLastSection(True)
self.pushButton_2.clicked.connect(self.close)
db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
db.setDatabaseName('formuladatabase')
db.open()
self.projectModel = QtSql.QSqlQueryModel(self)
self.projectModel.setQuery("select rowid, Name, Surname from search",db)
self.tableView.setModel(self.projectModel)
self.tableView.clicked.connect(self.handlebutton)
tableview works great just need to print that clicked row
def handlebutton(self):
rows = self.tableView.selectionModel().selectedIndexes()
print(self.projectModel.record(rows[0].row()).value("rowid").toInt())
self.newwindow = classformularesult(self)
self.newwindow.show()
been trying for a while but cant figure this one out.
Just a note: I noticed you tagged PyQt5, but the docs aren't so good there, and I'm pretty confident all I'm stating here still applies (from PyQt4).
It appears as though you're assuming that rows is going to contain the data from your query, which it does not. selectedRows(),selectedColumns,selectedIndexes() (documented here: http://pyqt.sourceforge.net/Docs/PyQt4/qitemselectionmodel.html#selectedRows) all return type list-of-QModelIndex, which are basically indexes.. not data.
list-of-QModelIndex QItemSelectionModel.selectedRows (self, int column
= 0)
Returns the indexes in the given column for the rows where all columns
are selected.
I think it's easier to use selectedIndexes
list-of-QModelIndex QItemSelectionModel.selectedIndexes (self)
Returns a list of all selected model item indexes. The list contains
no duplicates, and is not sorted.
To get the data, model.record.value() returns a QVariant, to which you have to cast to the proper type for printing. So, in your case:
rows = self.tableView.selectionModel().selectedIndexes()
print self.projectModel.record(rows[0].row()).value("rowid").toInt()
print self.projectModel.record(rows[0].row()).value("Name").toString()
print self.projectModel.record(rows[0].row()).value("Surname").toString()
The rows[0].row() is accessing element 0 of the "list-of-QModelIndex" type that's returned, and QModelIndex (http://pyqt.sourceforge.net/Docs/PyQt4/qmodelindex.html#details) has a row() method which returns the index of the row. Given the way you've set it up, the "list-of-QModelIndex" list should always be a single element list (you wired it up with the "clicked" signal), so the row[0] should return the proper element.
See here for additional information:
http://ftp.ics.uci.edu/pub/centos0/ics-custom-build/BUILD/PyQt-x11-gpl-4.7.2/doc/html/qtsql.html
About half way down there's a "Using the SQL Model Classes" heading with some good examples.
for the record and other in my situation the answers was
def handlebutton(self):
rows = self.tableView.selectionModel().selectedIndexes()
print(self.projectModel.record(rows[0].row()).value("rowid"))
self.newwindow = classformularesult(self)
self.newwindow.show()
thank you SEGFAULTCODER
I am using widgets in IPython that allows the user to repeatedly search for a phrase and see the results (different titles) in another widget (a selection widget) and then select one of the results.
In short:
search_text = widgets.Text(description = 'Search')
search_result = widgets.Select(description = 'Select table')
def search_action(sender):
phrase = search_text.value
df = search(phrase) # A function that returns the results in a pandas df
titles = df['title'].tolist()
search_result.options = titles
search_text.on_submit(search_action)
This used to work fine, but after updating to the latest version of ipywidgets (5.1.3 from 4.0.1) it seems like
search_selection.options = titles
Produce the following errors (one or both, it varies):
TraitError: Invalid selection
TypeError: 'list' object is not callable
It still works in the sense that the widget gets updated with the results based on the search from the other widget, but it gives an error.
What is the correct way of setting the options in one widget based on the results from another widget?
(edit: added more detailed error message)
I encountered this exact problem an hour ago. I have hacked together a solution using the minimum example here: Dynamically changing dropdowns in IPython notebook widgets and Spyre, since my own requirements were to have dynamically linked lists. I am sure you'll be able to adapt your requirements using this solution.
The key is to pre-generate all Dropdowns/Select. For some reason, w.options = l only sets w._options_labels but not w.options. Subsequent validation of the selected value of w will then fail horribly.
import ipywidgets as widgets
from IPython.display import display
geo={'USA':['CHI','NYC'],'Russia':['MOW','LED']}
geoWs = {key: widgets.Select(options=geo[key]) for key in geo}
def get_current_state():
return {'country': i.children[0].value,
'city': i.children[1].value}
def print_city(**func_kwargs):
print('func_kwargs', func_kwargs)
print('i.kwargs', i.kwargs)
print('get_current_state', get_current_state())
def select_country(country):
new_i = widgets.interactive(print_city, country=countryW, city=geoWs[country['new']])
i.children = new_i.children
countryW = widgets.Select(options=list(geo.keys()))
init = countryW.value
cityW = geoWs[init]
countryW.observe(select_country, 'value')
i = widgets.interactive(print_city, country=countryW, city=cityW)
display(i)
Note lastly that it is not trivial to obtain the most up-to-date state of the widgets. These are
directly from the children's values, via get_current_state. This can be trusted.
from the interactive instance, via i.kwargs
from the supplied args to print_city
The latter two can sometimes be out of date, for various reasons I don't wish to find out further.
Hope this helps.
You can hold notifications for during the assignment to options:
with search_result.hold_trait_notifications():
search_result.options = titles
Thus:
search_text = widgets.Text(description = 'Search')
search_result = widgets.Select(description = 'Select table')
def search_action(sender):
phrase = search_text.value
df = search(phrase) # A function that returns the results in a pandas df
titles = df['title'].tolist()
with search_result.hold_trait_notifications():
search_result.options = titles
See hmelberg's explanation below
"The root of the error is that the widget also has a value property and the value may not be in the new list of options. Because of this, widget value may be "orphaned" for a short time and an error is produced."
I had a similar problem and solved it Registering callbacks to trait changes in the kernel
caption = widgets.Label(value='The values of range1 and range2 are synchronized')
slider = widgets.IntSlider(min=-5, max=5, value=1, description='Slider')
def handle_slider_change(change):
caption.value = 'The slider value is ' + (
'negative' if change.new < 0 else 'nonnegative'
)
slider.observe(handle_slider_change, names='value')
display(caption, slider)
I guess this solution wasn't available back in 2016 or my problem wasn't as similar as thought.
The root of the error is that the widget also has a value property and the value may not be in the new list of options. Because of this, widget value may be "orphaned" for a short time and an error is produced.
The solution is either to assign the widget value to the option list before assigning it to the widget (and remove the value/option after if desired), or as Dan writes: use create a hold_trait-notifications()
Dan's approach is the best. The above just explains the cause of the problem.
i am currently designing a QT-gui in Python and i want to allow the user to switch QListWidgetItems between two QListWidgets. Multiple selection (CTRL) is allowed and switching is done by two control buttons.
In the QT4-Designer the lists looks like this
So if the user selects for example two items from the left list and clicks on the '>' Button the items have to be added to the right list and consequently deleted from the left list.
My current triggered Button-Events look like this:
def switchR( self ):
itemlistSel = self.list_left.selectedItems()
for item in itemlistSel:
self.list_right.addItem( item )
self.list_left.removeItemWidget( item )
But nothing happens? Someone got a quick solution?
The removeItemWidget() method doesn't quite do what you're expecting it to do (see docs). Use takeItem(), addItem() and row() instead:
def switch(self):
items = self.left.selectedItems()
for item in items:
n = self.left.row(item) # get the index/row of the item
i = self.left.takeItem(n) # pop
self.right.addItem(i) # add to right QListWidget
I am working on python plugins.I used PyQt4 Designer.
I want to list query result into QTreeWidget.
My code is as follows:
c = self.db.con.cursor()
self.db._exec_sql(c, "select est from bio")
for row in c.fetchall():
item_value=unicode(row[0])
top_node1 = QTreeWidgetItem(item_value)
self.treeWidget.insertTopLevelItem(0, top_node1)
The query returns the values as:
But when i list these values into QTreeWidget using above code,it is shown as below :
Only first character is shown.If i change '0' to some other number in self.treeWidget.insertTopLevelItem(0, top_node1) ,nothing appears in QTreeWidget.
How do i do it????
thanx.
If you take a look at the documentation for a QTreeWidgetItem, you will see there are a number of possible constructors for creating an instance. Though none of which it seems you are using in a way that is going to give you desirable results. The closest match to the signature you are providing is:
QTreeWidgetItem ( const QStringList & strings, int type = Type )
What this is probably doing is taking your string (I am assuming row[0] is a string because I don't know which drivers you are using) and applying it as a sequence, which would fullfill the requiremets of QStringList. Thus what you are getting is populating multiple columns of your item with each letter of your string value. If this is what you wanted, then you would n eed to tell your widget to show more columns: self.treeWidget.setColumnCount(10). But this isn't what you are looking for I am sure.
More likely what you should be trying is to create a new item, then add the value to the desired column:
item = QTreeWidgetItem()
item.setText(0, unicode(row[0]))
self.treeWidget.insertTopLevelItem(0, item)
You can use the default constructor with no arguments, set the text value of the first column to your database record field value, and then add that item to the tree. You could also build up a list of the items and add them at once:
items = []
for row in c.fetchall():
item = QTreeWidgetItem()
item.setText(0, unicode(row[0]))
items.append(item)
self.treeWidget.insertTopLevelItems(0, items)
Your first aproach could be corrected just add a list to the widgetitem not a string like this:
top_node1 = QTreeWidgetItem([item_value])