I need to add new columns to a Tkinter TreeView widget after creating it, but I can't find a way to do it. I've tried using the configure method to modify the columns attribute of the tree, but this resets all columns except the icon column.
The only solution I see is to configure it to have as many columns as I can possibly need and make them all invisible, so that I can make them visible when I need to add one. Is there a better way?
Newbie here:
I had same issue as you. I solved it as below.
hint: Initialise the Treeview 'after' you have your column names available. I couldn't find any solution on the web to add new columns after the initialisation of the Treeview is done.
Example:
--blah bhah code---
def treecols(self,colnames=[],rowdata=[]):
self.tree = ttk.Treeview ( self.frame1,columns=colnames )
self.tree.grid ( )
for eachcol in colnames:
self.tree.heading(column=eachcol,text=eachcol)
self.tree.column(column=eachcol,width=100,minwidth=0)
All the magic is in the add_columns method, the rest is just get a working example.
I hope this answers your question (a bit late but it might help others).
import tkinter
import tkinter.ttk
class GUI():
def __init__(self, master):
self.view = tkinter.ttk.Treeview(master)
self.view.pack()
self.view.heading('#0', text='Name')
self.view.insert('', 'end', text='Foo')
self.view.insert('', 'end', text='Bar')
self.view['columns'] = ('foo')
self.view.heading('foo', text='foo')
self.view.set(self.view.get_children()[0], 'foo', 'test')
self.add_columns(('bar', 'blesh'))
def add_columns(self, columns, **kwargs):
# Preserve current column headers and their settings
current_columns = list(self.view['columns'])
current_columns = {key:self.view.heading(key) for key in current_columns}
# Update with new columns
self.view['columns'] = list(current_columns.keys()) + list(columns)
for key in columns:
self.view.heading(key, text=key, **kwargs)
# Set saved column values for the already existing columns
for key in current_columns:
# State is not valid to set with heading
state = current_columns[key].pop('state')
self.view.heading(key, **current_columns[key])
tk = tkinter.Tk()
GUI(tk)
tk.mainloop()
Related
I am trying to create a drop down menu in tkinter that allows the user to select a machine, or row # from the excel sheet, and then all the data from that entire row is displayed in tkinter through the display_selected function. I keep getting this error.
This is my error:
TypeError: line_7_window.<locals>.display_selected() takes 0 positional arguments but 1 was given
This is my code:
def get_machine():
for row in sheet.iter_rows(min_row=2, max_col=1, max_row=60):
for cell in row:
if cell.value == inputmachine:
machineindex = cell
return machineindex.row
def get_machineattributes():
for col in sheet.iter_cols(min_row = (get_machine()), max_col = 15, max_row = (get_machine())):
for cell in col:
return (cell.value)
def display_selected():
data = Message(line_7, text=get_machineattributes())
data.pack()
data.place(x=650, y=30)
copy = Button(line_7, text="Copy to Clipboard", command=pyperclip.copy(line7_choice))
copy.pack()
copy.place(x=550, y=45)
return
inputmachine = StringVar(line_7)
inputmachine.set("Click to select a machine")
dropdown = OptionMenu(line_7, inputmachine, *lst, command=display_selected)
dropdown.pack()
dropdown.place(x=670, y=25)
I have tried everything and I cant figure out why this would not work.
Your callback function display_selected, specified when creating the option menu, will receive the actual option chosen from Tk, so you need that parameter when defining it, even if you do nothing with it.
In other words, use something like:
def display_selected(choice):
del choice # not used
# rest of function
As an aside, I suspect you may be better off with an option menu to select the item, and a separate button to act on that selection. That would allow you to confirm the selected item before performing any action, in case you inadvertently choose the wrong one.
I made PyQt5 APP which create SQlite3 DB and show data from it in QTableView widget.
I have a problem with editing rows in a widget. There are 3 buttons "Add", "Change" and "Delete" that should delete, modify and add new rows to the widget, as well as edit the database itself, but the buttons do not work properly.
"Add" button - after clicking add button, when all new data inputted and Enter clicked, all values is gone and "!" symbol is showing. I need press Add button, input new data in cells, click Enter and all data must save in SQL DB and displayed in QTableView widget in live time.
"Change" button - when change clicked and Enter pressed after cell editing, all data gone. All data must be save in real time is SQL DB after change button press.
3)"Delete" button - this button don't delete row. There is no error, no any answer from app.
How to program the buttons correctly ?
Do I need to use a delegate?
Which programming approach is more preferable when working with a graphical interface and SQL database?
I made a reproducible example. At startup, a database with 1 table and 2 rows will be created.
import sys, os, sqlite3
from datetime import datetime
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtSql import *
from PyQt5.QtCore import *
CONFIG_NAME = 'config.ini'
DB_NAME = 'nsi.db'
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__()
self.window_pref()
self.show_widgets()
def window_pref(self):
self.setWindowTitle('PyQt5 APP')
self.def_width = 800
self.def_height = 400
self.def_size = self.setMinimumSize(self.def_width, self.def_height)
def show_widgets(self):
self.createConnection()
self.fillDB()
self.setupMainWidgets()
def createConnection(self):
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(DB_NAME)
if not db.open():
QMessageBox.warning(self, 'PyQt5 APP',
'Error:{}'.format(db.lastError().text()))
sys.exit(1)
def fillDB(self):
query = QSqlQuery()
query.exec_("""\
CREATE TABLE sprav (
id_nsi INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
nsi_name TEXT UNIQUE NOT NULL,
file_date TEXT NOT NULL,
file_name TEXT NOT NULL)
""")
query.prepare("""\
INSERT INTO sprav (nsi_name, file_date, file_name)VALUES (?, ?, ?)
""")
sample_list = (('nsi1', 'january', 'file1'), ('nsi2', 'may', 'file2'))
for i in sample_list:
query.addBindValue(i[0])
query.addBindValue(i[1])
query.addBindValue(i[2])
query.exec_()
def setupMainWidgets(self):
mw_widget = QWidget()
main_panel = QHBoxLayout(mw_widget)
# SQL Table
self.modelSql = QSqlTableModel()
self.modelSql.setTable('sprav')
self.modelSql.setQuery(QSqlQuery(
'SELECT nsi_name, file_date, file_name FROM sprav'))
self.modelSql.setHeaderData(self.modelSql.fieldIndex('nsi_name'),
Qt.Horizontal, 'Name')
self.modelSql.setHeaderData(self.modelSql.fieldIndex('file_date'),
Qt.Horizontal, 'Date')
self.modelSql.setHeaderData(self.modelSql.fieldIndex('file_name'),
Qt.Horizontal, 'File')
self.modelSql.setEditStrategy(QSqlTableModel.OnFieldChange)
self.modelSql.select()
# QTableView()
self.table_view = QTableView()
self.table_view.setSelectionBehavior(1)
self.table_view.setAlternatingRowColors(True)
self.table_view.setModel(self.modelSql)
self.table_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
main_panel.addWidget(self.table_view)
# QVBoxLayout()
right_panel = QVBoxLayout()
line = QFrame()
line.setFrameShape(QFrame.HLine)
self.add_record = QPushButton('Add', self)
self.add_record.clicked.connect(self.addRecord)
self.change_record = QPushButton('Change', self)
self.change_record.clicked.connect(self.changeRecord)
self.delete_record = QPushButton('Delete', self)
self.delete_record.clicked.connect(self.delRecord)
right_panel.addSpacing(20)
right_panel.addWidget(line)
right_panel.addWidget(self.add_record)
right_panel.addWidget(self.change_record)
right_panel.addWidget(self.delete_record)
right_panel.addStretch()
main_panel.addLayout(right_panel)
self.setCentralWidget(mw_widget)
def addRecord(self):
row = self.modelSql.rowCount()
self.modelSql.insertRow(row)
index = self.modelSql.index(row, 0)
self.table_view.setCurrentIndex(index)
self.table_view.edit(index)
def delRecord(self):
cur_item = self.table_view.selectedIndexes()
for index in cur_item:
self.modelSql.removeRow(index.row())
self.modelSql.select()
def changeRecord(self):
self.table_view.edit(self.table_view.currentIndex())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
There are two problems with your code.
First of all, if you use QSqlTableModel, you should not call setQuery():
This function simply calls QSqlQueryModel::setQuery(query). You should normally not call it on a QSqlTableModel. Instead, use setTable(), setSort(), setFilter(), etc., to set up the query.
This is the main reason for which your row addition/deletion/editing didn't work: the model became partially invalid, and any submission was discarded because columns didn't match the records of the table model, which is very important also because the model requires an incremental key that QSqlTableModel is able to use properly on its own.
Remove the setQuery() from your code, and consider that if you did that only to hide a column, then you just have to hide that column:
self.table_view.setColumnHidden(0, True)
Obviously, you have to keep in mind that all column indexes you will use, will now start from 1, since the model also includes the id:
def addRecord(self):
# ...
index = self.modelSql.index(row, 1)
self.table_view.setCurrentIndex(index)
self.table_view.edit(index)
The other problem was the deletion of rows: even after fixing what described above, the number of rows and its order would have been wrong:
when you cycle through the selectedIndexes() you're getting the same row for each selected item: since you used the SelectRows selection behavior, the for loop would have called removeRow() three times the same row, for each selected row;
removal of indexes should always be in reverse, sorted order; consider if you try to remove row 0 and 1: the first iteration would remove row 0, but at that point the previous row 1 would have become the new row 0, so the next iteration would actually delete the row that previously was the third;
The solution is to have a sorted list of unique row numbers, and cycle through them in reversed order:
def delRecord(self):
# create a set of unique row numbers
rows = set([i.row() for i in self.table_view.selectedIndexes()])
# cycle through them in reversed sorting order
for row in sorted(rows, reverse=True):
self.modelSql.removeRow(row)
self.modelSql.select()
Remember to call select() (I know you did, but better safe than sorry), as explained in the documentation about removeRows():
Deletions are submitted immediately to the database. The model retains a blank row for successfully deleted row until refreshed with select().
Unrelated notes: 1. avoid unnecessary and confusing imports: since you're already importing QtWidgets with wildcard, there's no point for from PyQt5 import QtWidgets; you either import the submodule, or its classes; 2. setMinimumSize returns nothing, so self.def_size will be None; if you want to keep a variable for the default size, use self.def_size = QSize(self.def_width, self.def_height) then self.setMinimumSize(self.def_size); 3. do not use sys.exit() inside a Qt app (and from a QWidget class), instead use QApplication.exit(1); 4. use more verbose names that also clarify their type: you have almost identical names for buttons and functions (add_record and addRecord), which is a poor choice in naming (also considering the different writing style): a better choice would be to name the button like add_record_btn, or addRecordBtn to follow the Qt convention;
I am writing a UI for a simulation program which accepts tabular data.
The basic functionality I need is for the user to be able to enter / change data in cells either by directly typing into them, or by pasting data (usually from an excel sheet). The program checks this data and either accepts or rejects it before running the simulation. I also want to let the user type in their own column headers for the table.
Tksheet is an awesome Tkinter add-on, giving an excel-like "feel" to the input frame, but its documentation leaves much to be desired. (For instance: each event generates a different event-information array--see code for two event-processing routines below--but nowhere is it specified what these parameters are. It is left for the user to discover using trial and error, or trying to read the source code--which is not documented either).
I have two specific questions:
Is there any way to not-commit, or to roll back, changes to the table? If my data-tests fail, how do I prevent potentially harmful user input from being entered into the table?
Obviously I can (and do) add a begin_*** event in which I can keep copies of the original values, and then reset the table values if the data testing at the end_*** event fails, but this is wasteful and inelegant. I have a feeling that the set_data_ref_on_destroy property has something to do with such a capability, but the documentation does not explain what this parameter is or how to use it.
How can I change a single column header at a time? The .headers property seems to work only with a full list of headers starting with column 0 (if I run self.sheet.headers([single_value], index = i) it ignores the index parameter and plugs single_value in column 0)
Again, I can set the column headers to something non-default at init and keep a running list of all headers, so that I can reset all the headers on each change, but this is wasteful and inelegant.
In the following code sample I set up a simple table, and bind three user-generated events: one for typing a value to a cell, one for pasting a block of values, and one for adding an option to the right-click menu of a column header, to allow the user to type a name to the column.
from tksheet import Sheet
import tkinter as tk
import tkinter.messagebox as msg
import tkinter.simpledialog as sd
class demo(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.grid_columnconfigure(0, weight=1) # This configures the window's escalators
self.grid_rowconfigure(0, weight=1)
self.frame = tk.Frame(self)
self.frame.grid_columnconfigure(0, weight=1)
self.frame.grid_rowconfigure(0, weight=1)
self.frame.grid(row=0, column=0, sticky="nswe")
self.sheet = Sheet(self.frame, data=[[]]) # set up empty table
self.sheet.grid(row=0, column=0, sticky="nswe")
self.sheet.enable_bindings(bindings= # enable table behavior
("single_select",
"select_all",
"column_select",
"row_select",
"drag_select",
"arrowkeys",
"column_width_resize",
"double_click_column_resize",
"row_height_resize",
"double_click_row_resize",
"right_click_popup_menu",
"rc_select", # rc = right click
"copy",
"cut",
"paste",
"delete",
"undo",
"edit_cell"
))
# Note that options that change the structure/size of the table (e.g. insert/delete col/row) are disabled
# make sure that pasting data won't change table size
self.sheet.set_options(expand_sheet_if_paste_too_big=False)
# bind specific events to my own functions
self.sheet.extra_bindings("end_edit_cell", func=self.cell_edited)
self.sheet.extra_bindings("end_paste", func=self.cells_pasted)
label = "Change column name" # Add option to the right-click menu for column headers
self.sheet.popup_menu_add_command(label, self.column_header_change, table_menu=False, index_menu=False, header_menu=True)
# Event functions
def cell_edited(self, info_tuple):
r, c, key_pressed, updated_value = info_tuple # break the info about the event to individual variables
if check_input(updated_value):
pass # go do stuff with the updated table
else:
msg.showwarning("Input Error", "'" + updated_value + "' is not a legal value")
pass # what do I do here? How do I make tksheet *not* insert the change to the table?
def cells_pasted(self, info_tuple):
key_pressed, rc_tuple, updated_array = info_tuple # break the info about the event to individual variables
r, c = rc_tuple # row & column where paste begins
if check_input(updated_array):
pass # go do stuff with the updated table
else:
msg.showwarning("Input Error", "pasted array contains illegal values")
pass # what do I do here? How do I make tksheet *not* insert the change to the table?
def column_header_change(self):
r, c = self.sheet.get_currently_selected()
col_name = sd.askstring("User Input", "Enter column name:")
if col_name is not None and col_name != "": # if user cancelled (or didn't enter anything), do nothing
self.sheet.headers([col_name], index=c) # This does not work - it always changes the 1st col
self.sheet.redraw()
# from here down is test code
def check_input(value): # instead of actual data testing we let the tester choose a pass/fail response
return msg.askyesno("Instead of input checking","Did input pass entry checks?")
test = demo()
lst = ["hello", "world"]
test.sheet.insert_column(values=lst)
lst = [0, "hello", "yourself"]
test.sheet.insert_column(values=lst)
test.mainloop()
I realize that the original post is now 5 months old, and I'm a relative n00b, but I hope this helps.
Given a tksheet instance 'sheet' that has already been populated with headers ["A"."B"."C"], the following works to change the header "B" to "NEW":
sheet.headers()[1]="NEW"
Hope this helps.
I am trying to create a table, where it has 2 columns and several rows.
Column1 will be listing all the available mesh/geos in the scene while Column2 will be in the form of combo box per cell where it contains several options - depending on the mesh/geos from Column1, it will lists different shader options in the combobox as it will be reading off from a file. Meaning to say in the table, each item is on a per-row basis.
I am currently having issues with populating the list of mesh/geos into Column1. Suppose my scene has 5 geos - pCube1, pCube2, pCube3, pCube4, pCube5, in my table, I would be expecting the Column0 of its 5 rows to be populated with pCube#, however instead of that, I got pCube5 as my output result instead.
Please see the following code:
from PyQt4 import QtGui, QtCore
from functools import partial
import maya.cmds as cmds
class combo_box( QtGui.QComboBox ):
# For combox
def __init__( self, *args, **kwargs ):
super( combo_box, self ).__init__( *args, **kwargs)
def get_all_geos():
all_geos = cmds.ls(type='mesh')
return all_geos
class TestTable( QtGui.QWidget ):
def __init__( self, parent=None ):
QtGui.QWidget.__init__( self, parent )
self.setLayout( QtGui.QVBoxLayout() )
self.resize( 600, 300 )
self.myTable = QtGui.QTableWidget()
self.myTable.setColumnCount( 2 )
rowCount = len(get_all_geos())
self.myTable.setRowCount(rowCount)
self.setTable()
self.layout().addWidget(self.myTable)
self.myTable.cellChanged.connect(self.update)
def setTable(self):
# Adding the list of mesh found in scene into first column
for geo in get_all_geos():
item = cmds.listRelatives(geo, parent=True)[0]
for i in range(0, self.myTable.rowCount()):
# instead of being populated with the list of items, I got the same name for the entire column
self.myTable.setItem(i, 0, QtGui.QTableWidgetItem(item))
# sets the combobox into the second column
box = combo_box()
nameList = ("test1","test2","test3")
box.addItems(nameList)
self.myTable.setCellWidget(i,1,box)
box.currentIndexChanged.connect(partial(self.tmp, i))
def tmp(self, rowIndex, comboBoxIndex):
item = "item " + str(comboBoxIndex)
self.myTable.setItem(rowIndex, 2, QtGui.QTableWidgetItem(item))
if __name__ == "__main__":
tableView = TestTable()
tableView.show()
In my setTable function, the item is not being processed correctly? when I am trying to add it into the QTableWidget. Can someone advise?
Additionally, if anyone could answers, does the format I have used, would it be applicable for the scenario I am trying to achieve as I mentioned at the start of the post?
In your setTable() method, you are looping through the geometries, then you are looping through the rows. Since each geometry represents a row you only really need to loop through them and remove the other loop.
Modifying it like so fixes the output:
def setTable(self):
# Adding the list of mesh found in scene into first column
geos = get_all_geos()
for i in range(0, len(geos)):
item = cmds.listRelatives(geos[i], parent=True)[0]
# instead of being populated with the list of items, I got the same name for the entire column
self.myTable.setItem(i, 0, QtGui.QTableWidgetItem(item))
# sets the combobox into the second column
box = combo_box()
nameList = ("test1","test2","test3")
box.addItems(nameList)
self.myTable.setCellWidget(i,1,box)
box.currentIndexChanged.connect(partial(self.tmp, i))
The reason it was failing was because your second loop kept overriding the rows with the last geo in the list.
I've got some code in a class that extends gtk.TreeView, and this is the init method. I want to create a tree view that has 3 columns. A toggle button, a label, and a drop down box that the user can type stuff into. The code below works, except that the toggle button doesn't react to mouse clicks and the label and the ComboEntry aren't drawn. (So I guess you can say it doesn't work). I can add rows just fine however.
#make storage enable/disable label user entry
self.tv_store = gtk.TreeStore(gtk.ToggleButton, str, gtk.ComboBoxEntry)
#make widget
gtk.TreeView.__init__(self, self.tv_store)
#make renderers
self.buttonRenderer = gtk.CellRendererToggle()
self.labelRenderer = gtk.CellRendererText()
self.entryRenderer = gtk.CellRendererCombo()
#make columns
self.columnButton = gtk.TreeViewColumn('Enabled')
self.columnButton.pack_start(self.buttonRenderer, False)
self.columnLabel = gtk.TreeViewColumn('Label')
self.columnLabel.pack_start(self.labelRenderer, False)
self.columnEntry = gtk.TreeViewColumn('Data')
self.columnEntry.pack_start(self.entryRenderer, True)
self.append_column(self.columnButton)
self.append_column(self.columnLabel)
self.append_column(self.columnEntry)
self.tmpButton = gtk.ToggleButton('example')
self.tmpCombo = gtk.ComboBoxEntry(None)
self.tv_store.insert(None, 0, [self.tmpButton, 'example label', self.tmpCombo])
First of all, you need to create a model with bool, str and str columns, not the way you are doing now. Second, you need to bind properties of renderers from appropriate model columns, e.g. as in
self.columnButton = \
gtk.TreeViewColumn ('Enabled', self.buttonRenderer,
active = 0) # 0 is the tree store column index
Then you need to set editable property on the renderer to True. And finally, you need to handle signals (changed or editing-done, depending on renderer type) yourself and update the store accordingly.
It may be easier to use some helpers, e.g.
Py-gtktree — there's even an example for editing a tree there.
Just connnect the toggled signal in the gtk.CellRendererToggle, when you click on it, it will emit that signal, then in your callback change the value in the model.
ej.
def toggle(self, cellrenderer, path):
Self.model[path][column] = not self.model[path][column]
self.model is the model asociated to the treeview,