PyQt: How to sort QTableView columns(strings and numericals) - excel

The line self.tableView.setSortingEnabled(True) sorts a table view when clicking on the header, but it sorts incorrectly. That is, it thinks every column is a string (e.g. it sorts numbers like 1,11,12,2,22,3, etc). How do I correct this?
My code:
self.model = QtGui.QStandardItemModel()
with open(file_name_temp, "rt") as fileInput:
i = 1
for row in csv.reader(fileInput):
item = QtGui.QStandardItem()
for field in row:
items = [
item.setData(field, QtCore.Qt.UserRole)
]
print(items)
self.model.appendRow(items)
tab_table_view = QtGui.QWidget()
self.Tab.insertTab(0, tab_table_view, self.File_Name)
self.tableView = QtGui.QTableView(tab_table_view)
self.tableView.setGeometry(QtCore.QRect(0, 0, 721, 571))
self.model = QtGui.QStandardItemModel(self)
self.tableView.setModel(self.model)
colll = self.Datas.dtypes.index
col_names = np.array(colll)
col_names = np.insert(col_names, 0, self.Datas.index.name)
self.model.setHorizontalHeaderLabels(col_names)
self.tableView.hideRow(0)
self.model.setSortRole(QtCore.Qt.UserRole)
Update 1:
if (".csv" or ".txt") in self.File_Name:
with open(file_name_temp, "rt") as fileInput:
i = 1
reader = csv.reader(fileInput)
next(reader, None)
for row in reader:
for x in range(0,Num_col+1):
try:
int(row[x])
row[x]=int(row[x])
except ValueError:
print('Not Int')
items = []
for field in row:
item = QtGui.QStandardItem(field)
if type(field)==int:
print('yyy')
data = int(field)
else:
data = field
item.setData(data, QtCore.Qt.UserRole)
items.append(item)
print(items)
self.model.appendRow(items)
gives the output as:
yyy
yyy
yyy
yyy
yyy
yyy
yyy
yyy
yyy
[<PyQt4.QtGui.QStandardItem object at 0x0000000006DF3948>, <PyQt4.QtGui.QStandardItem object at 0x0000000006DF38B8>, <PyQt4.QtGui.QStandardItem object at 0x0000000006DF3828>, <PyQt4.QtGui.QStandardItem object at 0x0000000006DF3798>, <PyQt4.QtGui.QStandardItem object at 0x0000000006DF3678>, <PyQt4.QtGui.QStandardItem object at 0x0000000006DF3EE8>, <PyQt4.QtGui.QStandardItem object at 0x0000000006DF3F78>, <PyQt4.QtGui.QStandardItem object at 0x00000000095D4048>, <PyQt4.QtGui.QStandardItem object at 0x00000000095D40D8>]
everything seems good in console but on the GUI window it does not show the table?

You don't show how you are creating the items for the model, but presumably you are doing something like this:
item = QtGui.QStandardItem(str(value))
where value is a python numeric type.
To get numeric sorting, set the values like this instead:
item = QtGui.QStandardItem()
item.setData(value, QtCore.Qt.DisplayRole)
But note that this will also make the table automatically use spin-boxes for editing cells, which you may not want. So an alternative solution would be:
item = QtGui.QStandardItem(str(value))
item.setData(value, QtCore.Qt.UserRole)
...
model.setSortRole(QtCore.Qt.UserRole)
Finally, for fully customised sorting, you can also subclass QStandardItem:
class StandardItem(QtGui.QStandardItem):
def __lt__(self, other):
return int(self.text()) < int(other.text())
item = StandardItem(str(value))
UPDATE:
Here is a demo script that reads csv files into a table, automatically converting the fields into the correct data-type for sorting:
import sys, csv
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.model = QtGui.QStandardItemModel(self)
self.model.setSortRole(QtCore.Qt.UserRole)
self.tableView = QtGui.QTableView()
self.tableView.setSortingEnabled(True)
self.tableView.setModel(self.model)
self.button = QtGui.QPushButton('Open CSV', self)
self.button.clicked.connect(self.handleButton)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.tableView)
layout.addWidget(self.button)
def handleButton(self):
path = QtGui.QFileDialog.getOpenFileName(
self, 'Open CSV', '', 'CSV files (*.csv *.txt)')
if path:
self.model.setRowCount(0)
with open(path) as stream:
reader = csv.reader(stream)
next(reader, None)
for row in reader:
items = []
for field in row:
item = QtGui.QStandardItem(field)
for numtype in (int, float):
try:
data = numtype(field)
break
except (ValueError, OverflowError):
pass
else:
print('Not a number: %r' % field)
data = field
item.setData(data, QtCore.Qt.UserRole)
items.append(item)
self.model.appendRow(items)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 150, 600, 400)
window.show()
sys.exit(app.exec_())

Related

Python integration of Objects in others

fix: I moved an invoke of a Radiobutton from the init of my view to the controller like this:
class Controller(object):
def __init__(self):
self.model = Model()
print('start init of View')
self.view = View(self.importieren, self.exportieren, self.beenden, self.J5, self.J6, self.J7, self.J8, self.J9,
self.J10, self.J11, self.J12, self.JA, self.tabelle_sorti, self.hinzufugen, self.zuordnen,
self.ande, self.JA, self.model.ausgabe('projekte'), self.tabelle_update, self.a1, self.a2, self.a3,
self.a4, self.a5, self.a6, self.a7)
print('done init')
self.wahlen = ('sErst', 'sZweit', 'sDritt')
self.delimiter = {'imp_s': None, 'imp_p': None, 'exp': None}
self.dchosen = None
self.slcsv = 'schuelerliste.csv'
self.plcsv = 'projektliste.csv'
self.double = False
self.andernx = ""
self.view.radios['jahrg-Alle'].invoke()
self.tabelle()
self.view.table['main'].bind('<Double-Button-1>', self.treevent)
# Erstimportierung
if self.model.ausgabe('schueler'):
self.importieren(True)
self.view.mainloop()
def tabelle_update(self, fetchshlr=None, fetchprj=None):
print('start update')
if fetchshlr is None:
fetchshlr = self.model.ausgabe('schueler')
if fetchprj is None:
fetchprj = self.model.ausgabe('projekte')
self.view.tableup(fetchshlr, fetchprj)
This invoke called the function, before View was ready and so caused the error. Thanks for the Help
I tried to not ask a question hear, but after hours of searching I have to.
I am currently working on a programm and recently splitted the main file in three in style of the MVC-Scheme. And I have one function to update a Treeview working as a table. But this function (and only that one!), says:
Exception in Tkinter callback\
Traceback (most recent call last):\
File "C:\Users\...\Python38\lib\tkinter\__init__.py", line 1883, in __call__\
return self.func(*args)\
File "C:/Users/.../Controller.py", line 184, in tabelle_update\
self.view.table['main'].tag_configure(i[0], background='white')\
AttributeError: 'Controller' object has no attribute 'view'
I already tried to use lambda (and if you ask why, because someone said it in the internet) and it just prevented the function to work at all. Also I think it has something todo with this:
{'model': <Model.Model object at 0x0000018CA8068160>, 'view': <View.View object .>, ...}
this is an extract of the attributes ditionary and I think it has something to do with the missing at 0x... part in 'view' as seen in 'model'
Please Help me to get this up and running
And here are my important code parts (if you need more, pls write)
note that the diffrent classes are in diffrent files and are properly imported
Major Changes in programmcode under tabelle_update and under the View class tableup
I tried to move the View heavy parts to View, didn't fix anything.
class Controller(object):
def __init__(self):
self.model = Model()
self.view = View(self.importieren, self.exportieren, self.beenden, self.J5, self.J6, self.J7, self.J8, self.J9,
self.J10, self.J11, self.J12, self.tabelle_update, self.tabelle_sorti, self.hinzufugen,
self.zuordnen, self.ande, self.tabelle_update, self.model.ausgabe('projekte'),
self.tabelle_update, self.a1, self.a2, self.a3, self.a4, self.a5, self.a6, self.a7)
self.wahlen = ('sErst', 'sZweit', 'sDritt')
self.delimiter = {'imp_s': None, 'imp_p': None, 'exp': None}
self.dchosen = None
self.slcsv = 'schuelerliste.csv'
self.plcsv = 'projektliste.csv'
self.double = False
self.andernx = ""
self.tabelle()
self.view.table['main'].bind('<Double-Button-1>', self.treevent)
# Erstimportierung
if self.model.ausgabe('schueler'):
self.importieren(True)
print(self.__dict__)
self.view.mainloop()
def tabelle(self):
fetch = self.model.ausgabe('schueler')
self.view.shlr_tabelle(fetch)
def tabelle_update(self, fetchshlr=None, fetchprj=None):
print('start update')
if fetchshlr is None:
fetchshlr = self.model.ausgabe('schueler')
if fetchprj is None:
fetchprj = self.model.ausgabe('projekte')
self.view.tableup(fetchshlr, fetchprj)
class View(ttkthemes.ThemedTk):
def __init__(self, imp, exp, bee, j5, j6, j7, j8, j9, j10, j11, j12, ja, tabsort, hin, zord, ande, scht,
prjt, aktutable, a1, a2, a3, a4, a5, a6, a7):
# print(ttkthemes.THEMES) # zum Ausgeben der verfügbaren Themes
ttkthemes.ThemedTk.__init__(self, theme='breeze')
self.title("Projektwochenverwaltungsprogramm")
self.geometry('800x300')
self.minsize(800, 300)
self.resizable(width=True, height=True)
# bestimmen der Callbacks
self.callback_imp = imp
self.callback_exp = exp
self.callback_bee = bee
self.radiocom = {'jahrg': [j5, j6, j7, j8, j9, j10, j11, j12, ja], 'ande': [a1, a2, a3, a4, a5, a6, a7]}
self.tabelle_sorti = tabsort
self.callback_aktut = aktutable
self.callback_zord = zord
self.callback_hin = hin
self.callback_ande = ande
self.callback_scht = scht
self.callback_prjt = prjt
# Tabelle
self.scrollbars = {'main': Scrollbar(self.rahmen[2], orient="vertical")}
self.table = {'main': Treeview(self.rahmen[2], yscrollcommand=self.scrollbars['main'].set, height=200)}
self.scrollbars['main'].pack(side=RIGHT, fill=BOTH)
self.rahmen[1].pack()
self.rahmen['popup_pro'].pack(fill=X)
self.rahmen[2].pack(fill=BOTH, expand=True)
def shlr_tabelle(self, fetch):
ml = ['ID']
for name in self.names['schueler']:
ml.append(name)
ml.append('Zugeordned zu')
self.table['main']['columns'] = ml
self.table['main']['show'] = 'headings'
for i in range(len(ml)):
self.table['main'].column(ml[i], width=self.width['schueler'][i], minwidth=self.width['schueler'][i])
for i in range(len(ml)):
self.table['main'].heading(ml[i], text=ml[i], command=lambda col=i: self.tabelle_sorti(col, False, 'main'))
for t in fetch:
self.table['main'].insert('', t[0], t[0], values=t) # , tags=t[0]
self.scrollbars['main'].config(command=self.table['main'].yview)
self.table['main'].pack(fill=BOTH)
def prj_tabelle(self, fetch):
self.top['prjt'] = Toplevel()
self.top['prjt'].title('Projekte Liste')
self.top['prjt'].geometry('800x300')
self.top['prjt'].minsize(800, 300)
self.scrollbars['prj'] = Scrollbar(self.top['prjt'], orient="vertical")
self.table['prj'] = Treeview(self.top['prjt'], yscrollcommand=self.scrollbars['prj'].set, height=200)
ml = ['ID']
for name in self.names['projekte']:
ml.append(name)
self.table['prj']['columns'] = ml
self.table['prj']['show'] = 'headings'
for i in range(len(ml)):
self.table['prj'].column(ml[i], width=self.width['projekte'][i], minwidth=self.width['projekte'][i])
for i in range(len(ml)):
self.table['prj'].heading(ml[i], text=ml[i], command=lambda col=i: self.tabelle_sorti(col, False, 'prj'))
for t in fetch:
self.table['prj'].insert('', t[0], t[0], values=t) # , tags=t[0]
self.scrollbars['prj'].config(command=self.table['prj'].yview)
self.scrollbars['prj'].pack(side=RIGHT, fill=BOTH)
self.table['prj'].pack(fill=BOTH)
def tableup(self, fetchshlr, fetchprj):
print('läuft')
for i in fetchshlr:
self.view.table['main'].tag_configure(i[0], background='white')
self.view.table['main'].delete(*self.view.table['main'].get_children())
for t in fetchshlr:
self.view.table['main'].insert('', t[0], t[0], values=t) # , tags=t[0]
for ele in fetchshlr:
if ele[8] is None:
self.view.table['main'].tag_configure(ele[0], background='#fa6150')
try:
for i in fetchprj:
self.view.table['prj'].tag_configure(i[0], background='white')
self.view.table['prj'].delete(*self.view.table['prj'].get_children())
for t in fetchprj:
self.view.table['prj'].insert('', t[0], t[0], values=t) # , tags=t[0]
failed = self.model.prj_aktu()
for prj in failed:
self.view.table['prj'].tag_configure(prj, background='#fa6150')
except KeyError:
pass
You have to ask yourself when self.view is fully initialized, and when you're trying to access self.view. The error is telling you that you're trying to access it before you define it.
You should be able to visualize this by adding print statements like so:
print("before self.view is defined")
self.view = View(...)
print("after self.view is defined")
and then:
print("using self.view...")
self.view.table['main'].tag_configure(i[0], background='white')

Implementation of MVC Design Pattern in Python3

I am trying to implement MVC using Python3.8. I have used this https://www.tutorialspoint.com/python_design_patterns/python_design_patterns_model_view_controller.htm Python2's example for practice.
But, I am receiving the following error:
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
My code is as following:
model.py
import json
class Person:
def __init__(self, first = None, last = None):
self.first = first
self.last = last
def name(self):
return ('%s %s' %(self.first, self.last))
#classmethod
def getAll(self):
database = open('data.txt', 'r')
result = []
jsonList = json.loads(database.read())
for item in jsonList:
item = json.loads(item)
person = Person(item['first'], item['last'])
result.append(person)
return result
view.py
from model import Person
def showAllView(list):
print ('In our db we have %i users. Here they are:' % len(list))
for item in list:
print (item.name())
def startView():
print ('MVC - the simplest example')
print ('Do you want to see everyone in my db?[y/n]')
def endView():
print ('Goodbye!')
controller.py
from model import Person
import view
def showAll():
#gets list of all Person objects
people_in_db = Person.getAll()
return view.showAllView(people_in_db)
def start():
view.startView()
answer = input('Enter y or n')
if answer == 'y':
return showAll()
else:
return view.endView()
if __name__ == "__main__":
start()
Data.txt
[{
"first": "abc",
"last": "xyz"
}]
Please, guide me in this and help me find the error. Thanks in advance.
I have solved the problem myself. The main problem was loading JSON elements twice in model.py, like below:
jsonList = json.loads(database.read())
for item in jsonList:
item = json.loads(item)
Now I have solved it by removing item = json.loads(item).

how to store in a list or dictionary the date from Gtk.Calendar.get_date() and a text from Gtk.TextBuffer for that date in python

I am trying to create a calendar app in python.As for now I have manage to create the Gtk.window, a Gtk.Notebook with two pages (page1 month calendar, page 2 week calendar) added to the Gtk.window.I have used Gtk.Calendar for month and from signal 'day-selected' I used a function to get_date().Also I used a Gtk.TextView with Gtk.TextBuffer to input text (as a note).
First question : how to connect the date with the input text and store it in a list or dictionary so i can save it later to a file.
Second:I have activate details with self.calendar.set_property("show-details", True) and defined a function self.calendar.set_detail_func(self.cal_entry) for details.
def cal_entry(self, calendar, year, month, date):
if self.textbuffer is not None:
self.calendar.mark_day(self.day)
How to mark the days with notes(text in Gtk.TextBuffer) because the above function does not work correctly.
Below is a snippet of the code:
class Window(Gtk.ApplicationWindow):
def __init__(self, app):
super(Window, self).__init__(title="Live Calendar", application=app)
self.set_default_size(-1, 400)
self.set_resizable(False)
self.set_border_width(10)
self.create_notebook()
self.entry()
self.refresh_icon()
def refresh_icon(self):
#refreshing the icon of the app
print(get_filename())
subject_lines = read_file(subject)
index = [i for i in range(len(subject_lines)) \
if subject_lines[i].startswith("Icon=")][0]
subject_lines[index] = "Icon=" + get_filename() + "\n"
write_file(subject, subject_lines)
self.set_icon_from_file(get_filename())
timer = threading.Timer(60, self.refresh_icon)
timer.start()
def create_notebook(self):
# Open Buttton
self.openbutton = Gtk.Button("Open")
self.openbutton.set_tooltip_text("Open Notes")
self.openbutton.connect("clicked", self.on_open_clicked)
# Save Button
self.savebutton = Gtk.Button("Save")
self.savebutton.set_tooltip_text("Save Notes")
self.savebutton.connect("clicked", self.on_save_clicked)
# Header
self.header = Gtk.HeaderBar(title="Live Calendar")
self.header.set_property("show_close_button", True)
self.header.pack_start(self.openbutton)
self.header.pack_start(self.savebutton)
self.set_titlebar(self.header)
#page1 month calendar
self.page1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
self.calendar = Gtk.Calendar()
self.calendar.set_property("show-week-numbers", True)
self.calendar.set_detail_height_rows(2)
self.calendar.set_detail_width_chars(2)
self.calendar.set_property("show-details", True)
self.calendar.set_detail_func(self.cal_entry)
self.__connect_signals()
self.page1.add(self.calendar)
#note taking
self.sw = Gtk.ScrolledWindow()
self.sw.set_hexpand(True)
self.sw.set_vexpand(True)
self.textview = Gtk.TextView()
self.textview.set_editable(True)
self.textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
self.textbuffer = self.textview.get_buffer()
self.sw.add(self.textview)
self.page1.pack_start(self.sw, True, True, 0)
#page2 week calendar
......a lot of code.......
#create notebook
self.notebook = Gtk.Notebook()
self.notebook.set_tab_pos(0)
self.notebook.append_page(self.page1, Gtk.Label('Month'))
self.notebook.append_page(self.page2, Gtk.Label('Week'))
self.add(self.notebook)
def __connect_signals(self):
self.day_selected_handle = self.calendar.connect('day-selected', self.entry)
def entry(self, *args):
self.page1.remove(self.label)
self.year, self.month, self.day = self.calendar.get_date()
self.month = self.month + 1
self.entrydate = datetime.date(self.year, self.month, self.day)
self.notedate = self.entrydate.strftime("%d/%m/%y")
self.text = self.entrydate.strftime("%d/%m/%y write your notes here...")
self.label = Gtk.Label(self.text)
self.page1.pack_start(self.label, False, False, 0)
self.show_all()
def cal_entry(self, calendar, year, month, date):
if self.textbuffer is not None:
self.calendar.mark_day(self.day)
#save into file
def on_save_clicked(self, widget):
dialog = Gtk.FileChooserDialog("Save file", self,
Gtk.FileChooserAction.SAVE,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
response = dialog.run()
if response == Gtk.ResponseType.OK:
save_file = dialog.get_filename()
start_iter = self.textbuffer.get_start_iter()
end_iter = self.textbuffer.get_end_iter()
text = self.textbuffer.get_text(start_iter, end_iter, True)
with open(save_file, 'w') as f:
f.write(text)
elif response == Gtk.ResponseType.CANCEL:
dialog.destroy()
dialog.destroy()
# open and read the file
def on_open_clicked(self, widget):
dialog = Gtk.FileChooserDialog("Please choose a file", self,
Gtk.FileChooserAction.OPEN,
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
filter_text = Gtk.FileFilter()
filter_text.set_name("Text files")
filter_text.add_mime_type("text/plain")
dialog.add_filter(filter_text)
response = dialog.run()
if response == Gtk.ResponseType.OK:
selected_file = dialog.get_filename()
with open(selected_file, 'r') as f:
data = f.read()
self.textbuffer.set_text(data)
elif response == Gtk.ResponseType.CANCEL:
dialog.destroy()
dialog.destroy()
def quitApp(self, par):
app.quit()
class Application(Gtk.Application):
def __init__(self):
super(Application, self).__init__()
def do_activate(self):
self.win = Window(self)
self.win.show_all()
def do_startup(self):
Gtk.Application.do_startup(self)
app = Application()
app.run(sys.argv)
Any help will be appriciated.
Thanks in advance
For the first question: Depends, but probably a dictionary indexed by a triple for y/m/d
d = dict()
d[(2019, 10, 1)] = "your notes..."
Second question: The way details work is that cal_entry(self, calendar, year, month, day) must return a string to be displayed below the day, in the calendar. So you have to get the text from the dictionary above. You can do that with:
def cal_entry(self, calendar, year, month, day):
self.calendar.mark_day(day)
return dict.get((year, month, day))
Maybe this sample code can help:
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class NotePopover(Gtk.Popover):
def __init__(self, app):
Gtk.Popover.__init__(self)
self.set_size_request(200, 300)
grid = Gtk.Grid()
grid.set_orientation(Gtk.Orientation.VERTICAL)
scrolled_window = Gtk.ScrolledWindow()
self.text_view = Gtk.TextView()
scrolled_window.add(self.text_view)
grid.add(scrolled_window)
button = Gtk.Button(label='Add')
grid.add(button)
grid.show_all()
self.add(grid)
scrolled_window.set_vexpand(True)
scrolled_window.set_hexpand(True)
button.connect('clicked', self.on_button_clicked)
self.app = app
def do_popup(self):
self.popup()
def on_button_clicked(self, widget):
year = app.win.calendar.get_property('year')
month = app.win.calendar.get_property('month')
day = app.win.calendar.get_property('day')
start_iter = self.text_view.get_buffer().get_start_iter()
end_iter = self.text_view.get_buffer().get_end_iter()
app.notes_dict[(year, month, day)] = self.text_view.get_buffer().get_text(start_iter, end_iter, True)
app.win.do_update()
self.text_view.get_buffer().set_text("")
self.popdown()
class Window(Gtk.Window):
def __init__(self, app):
Gtk.Window.__init__(self)
self.set_title('Test GtkCalendar')
self.set_default_size(400, 300)
header_bar = Gtk.HeaderBar()
header_bar.set_show_close_button(True)
self.set_titlebar(header_bar)
button = Gtk.Button(label="Add note")
button.get_style_context().add_class('suggested-action')
button.connect('clicked', self.on_button_clicked)
header_bar.pack_start(button)
grid = Gtk.Grid()
grid.set_orientation(Gtk.Orientation.VERTICAL)
self.calendar = Gtk.Calendar()
scrolled_window = Gtk.ScrolledWindow()
self.text_view = Gtk.TextView()
self.add(grid)
grid.add(self.calendar)
grid.add(scrolled_window)
scrolled_window.add(self.text_view)
self.calendar.set_hexpand(True)
self.calendar.set_halign(Gtk.Align.CENTER)
scrolled_window.set_vexpand(True)
scrolled_window.set_hexpand(True)
self.popover = NotePopover(app)
self.popover.set_relative_to(button)
self.connect('destroy', Gtk.main_quit)
self.calendar.connect('day-selected', self.calendar_changed)
self.calendar.connect('month-changed', self.calendar_changed)
self.show_all()
self.app = app
def on_button_clicked(self, widget):
self.popover.do_popup()
def calendar_changed(self, widget):
self.do_update()
def do_update(self):
year = app.win.calendar.get_property('year')
month = app.win.calendar.get_property('month')
day = app.win.calendar.get_property('day')
text = app.notes_dict.get((year, month, day))
if text is not None:
self.text_view.get_buffer().set_text(text)
else:
self.text_view.get_buffer().set_text("")
class Application:
def __init__(self):
self.notes_dict = dict()
self.win = Window(self)
if __name__ == '__main__':
app = Application()
Gtk.main()

How to handle Drag and Drop Properly using PYQT QAbstractItemModel

Here is a code I ended up after two days of TreeView/Model madness. The subject appeared to be much more broad than I thought. I barely can spend so much time creating a singe widget. Anyway. The drag-and-drop functionality of TreeView items has been enabled. But other than few interesting printout there is not much there. The double click on an item allows the user to enter a new item name which won't be picked up.
EDITED A DAY LATER WITH A REVISED CODE.
It is now by 90% functional tool.
The user can manipulate the TreeView items by drag and dropping, creating/duplicating/deleting and renaming. The TreeView items are representing the directories or folders in hierarchical fashion before they are created on a drive by hitting 'Print' button (instead of os.makedirs() the tool still simply prints each directory as a string.
I would say I am pretty happy with the result. Thanks to hackyday and to everyone who responded and helped with my questions.
A few last wishes...
A wish number 01:
I wish the PrintOut() method would use a more elegant smarter function to loop through the TreeView items to build a dictionary that is being passed to make_dirs_from_dict() method.
A wish number 02:
I wish deleting the items would be more stable. By some unknown reason a tool crashes on third/fourth Delete button clicks. So far, I was unable to trace the problem down.
A wish number 03:
3. I wish everyone the best and thanks for your help :
import sys, os
from PyQt4 import QtGui, QtCore
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from copy import deepcopy
import cPickle
class TreeItem(object):
def __init__(self, name, parent=None):
self.name = QtCore.QString(name)
self.parent = parent
self.children = []
self.setParent(parent)
def setParent(self, parent):
if parent != None:
self.parent = parent
self.parent.appendChild(self)
else: self.parent = None
def appendChild(self, child):
self.children.append(child)
def childAtRow(self, row):
if len(self.children)>row:
return self.children[row]
def rowOfChild(self, child):
for i, item in enumerate(self.children):
if item == child: return i
return -1
def removeChild(self, row):
value = self.children[row]
self.children.remove(value)
return True
def __len__(self):
return len(self.children)
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self):
QtCore.QAbstractItemModel.__init__(self)
self.columns = 1
self.clickedItem=None
self.root = TreeItem('root', None)
levelA = TreeItem('levelA', self.root)
levelB = TreeItem('levelB', levelA)
levelC1 = TreeItem('levelC1', levelB)
levelC2 = TreeItem('levelC2', levelB)
levelC3 = TreeItem('levelC3', levelB)
levelD = TreeItem('levelD', levelC3)
levelE = TreeItem('levelE', levelD)
levelF = TreeItem('levelF', levelE)
def nodeFromIndex(self, index):
return index.internalPointer() if index.isValid() else self.root
def index(self, row, column, parent):
node = self.nodeFromIndex(parent)
return self.createIndex(row, column, node.childAtRow(row))
def parent(self, child):
# print '\n parent(child)', child # PyQt4.QtCore.QModelIndex
if not child.isValid(): return QModelIndex()
node = self.nodeFromIndex(child)
if node is None: return QModelIndex()
parent = node.parent
if parent is None: return QModelIndex()
grandparent = parent.parent
if grandparent==None: return QModelIndex()
row = grandparent.rowOfChild(parent)
assert row != - 1
return self.createIndex(row, 0, parent)
def rowCount(self, parent):
node = self.nodeFromIndex(parent)
if node is None: return 0
return len(node)
def columnCount(self, parent):
return self.columns
def data(self, index, role):
if role == Qt.DecorationRole:
return QVariant()
if role == Qt.TextAlignmentRole:
return QVariant(int(Qt.AlignTop | Qt.AlignLeft))
if role != Qt.DisplayRole:
return QVariant()
node = self.nodeFromIndex(index)
if index.column() == 0:
return QVariant(node.name)
elif index.column() == 1:
return QVariant(node.state)
elif index.column() == 2:
return QVariant(node.description)
else: return QVariant()
def supportedDropActions(self):
return Qt.CopyAction | Qt.MoveAction
def flags(self, index):
defaultFlags = QAbstractItemModel.flags(self, index)
if index.isValid(): return Qt.ItemIsEditable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags
else: return Qt.ItemIsDropEnabled | defaultFlags
def setData(self, index, value, role):
if role == Qt.EditRole:
if value.toString() and len(value.toString())>0:
self.nodeFromIndex(index).name = value.toString()
self.dataChanged.emit(index, index)
return True
def mimeTypes(self):
return ['bstream', 'text/xml']
def mimeData(self, indexes):
mimedata = QtCore.QMimeData()
bstream = cPickle.dumps(self.nodeFromIndex(indexes[0]))
mimedata.setData('bstream', bstream)
return mimedata
def dropMimeData(self, mimedata, action, row, column, parentIndex):
if action == Qt.IgnoreAction: return True
droppedNode=cPickle.loads(str(mimedata.data('bstream')))
droppedIndex = self.createIndex(row, column, droppedNode)
parentNode = self.nodeFromIndex(parentIndex)
newNode = deepcopy(droppedNode)
newNode.setParent(parentNode)
self.insertRow(len(parentNode)-1, parentIndex)
self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex)
return True
def insertRow(self, row, parent):
return self.insertRows(row, 1, parent)
def insertRows(self, row, count, parent):
self.beginInsertRows(parent, row, (row + (count - 1)))
self.endInsertRows()
return True
def removeRow(self, row, parentIndex):
return self.removeRows(row, 1, parentIndex)
def removeRows(self, row, count, parentIndex):
self.beginRemoveRows(parentIndex, row, row)
node = self.nodeFromIndex(parentIndex)
node.removeChild(row)
self.endRemoveRows()
return True
class GUI(QtGui.QDialog):
def build(self, myWindow):
myWindow.resize(600, 400)
self.myWidget = QWidget(myWindow)
self.boxLayout = QtGui.QVBoxLayout(self.myWidget)
self.treeView = QtGui.QTreeView()
self.treeModel = TreeModel()
self.treeView.setModel(self.treeModel)
self.treeView.expandAll()
self.treeView.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.treeView.connect(self.treeView.model(), SIGNAL("dataChanged(QModelIndex,QModelIndex)"), self.onDataChanged)
QtCore.QObject.connect(self.treeView, QtCore.SIGNAL("clicked (QModelIndex)"), self.treeItemClicked)
self.boxLayout.addWidget(self.treeView)
self.PrintButton= QtGui.QPushButton("Print")
self.PrintButton.clicked.connect(self.PrintOut)
self.boxLayout.addWidget(self.PrintButton)
self.DeleteButton= QtGui.QPushButton("Delete")
self.DeleteButton.clicked.connect(self.DeleteLevel)
self.boxLayout.addWidget(self.DeleteButton)
self.insertButton= QtGui.QPushButton("Insert")
self.insertButton.clicked.connect(self.insertLevel)
self.boxLayout.addWidget(self.insertButton)
self.duplicateButton= QtGui.QPushButton("Duplicate")
self.duplicateButton.clicked.connect(self.duplicateLevel)
self.boxLayout.addWidget(self.duplicateButton)
myWindow.setCentralWidget(self.myWidget)
def make_dirs_from_dict(self, dirDict, current_dir='/'):
for key, val in dirDict.items():
#os.mkdir(os.path.join(current_dir, key))
print "\t\t Creating directory: ", os.path.join(current_dir, key)
if type(val) == dict:
self.make_dirs_from_dict(val, os.path.join(current_dir, key))
def PrintOut(self):
result_dict = {}
for a1 in self.treeView.model().root.children:
result_dict[str(a1.name)]={}
for a2 in a1.children:
result_dict[str(a1.name)][str(a2.name)]={}
for a3 in a2.children:
result_dict[str(a1.name)][str(a2.name)][str(a3.name)]={}
for a4 in a3.children:
result_dict[ str(a1.name)][str(a2.name)][str(a3.name)][str(a4.name)]={}
for a5 in a4.children:
result_dict[ str(a1.name)][str(a2.name)][str(a3.name)][str(a4.name)][str(a5.name)]={}
for a6 in a5.children:
result_dict[str(a1.name)][str(a2.name)][str(a3.name)][str(a4.name)][str(a5.name)][str(a6.name)]={}
for a7 in a6.children:
result_dict[str(a1.name)][str(a2.name)][str(a3.name)][str(a4.name)][str(a5.name)][str(a6.name)][str(a7.name)]={}
self.make_dirs_from_dict(result_dict)
def DeleteLevel(self):
if len(self.treeView.selectedIndexes())==0: return
currentIndex = self.treeView.selectedIndexes()[0]
currentRow=currentIndex.row()
currentColumn=currentIndex.column()
currentNode = currentIndex.internalPointer()
parentNode = currentNode.parent
parentIndex = self.treeView.model().createIndex(currentRow, currentColumn, parentNode)
print '\n\t\t\t CurrentNode:', currentNode.name, ', ParentNode:', currentNode.parent.name, ', currentColumn:', currentColumn, ', currentRow:', currentRow
# self.treeView.model().removeRow(len(parentNode)-1, parentIndex)
self.treeView.model().removeRows(currentRow, 1, parentIndex )
#self.treeView.model().removeRow(len(parentNode), parentIndex)
#self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex)
def insertLevel(self):
if len(self.treeView.selectedIndexes())==0: return
currentIndex = self.treeView.selectedIndexes()[0]
currentNode = currentIndex.internalPointer()
newItem = TreeItem('Brand New', currentNode)
self.treeView.model().insertRow(len(currentNode)-1, currentIndex)
self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), currentIndex, currentIndex)
def duplicateLevel(self):
if len(self.treeView.selectedIndexes())==0: return
currentIndex = self.treeView.selectedIndexes()[0]
currentRow=currentIndex.row()
currentColumn=currentIndex.column()
currentNode=currentIndex.internalPointer()
parentNode=currentNode.parent
parentIndex=self.treeView.model().createIndex(currentRow, currentColumn, parentNode)
parentRow=parentIndex.row()
parentColumn=parentIndex.column()
newNode = deepcopy(currentNode)
newNode.setParent(parentNode)
self.treeView.model().insertRow(len(parentNode)-1, parentIndex)
self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex)
print '\n\t\t\t CurrentNode:', currentNode.name, ', ParentNode:', parentNode.name, ', currentColumn:', currentColumn, ', currentRow:', currentRow, ', parentColumn:', parentColumn, ', parentRow:', parentRow
self.treeView.update()
self.treeView.expandAll()
def treeItemClicked(self, index):
print "\n clicked item ----------->", index.internalPointer().name
def onDataChanged(self, indexA, indexB):
print "\n onDataChanged NEVER TRIGGERED! ####################### \n ", index.internalPointer().name
self.treeView.update(indexA)
self.treeView.expandAll()
self.treeView.expanded()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
myWindow = QMainWindow()
myGui = GUI()
myGui.build(myWindow)
myWindow.show()
sys.exit(app.exec_())
I am not totally sure what you are trying to achieve, but it sounds like you want to retrieve the dragged item in the drop operation, and have double click save a new node name.
Firstly, you need to save the dragged item into the mimeData. Currently, you are only saving the string 'mimeData', which doesn't tell you much. The mimeType string that it is saved as (here I used 'bstream') can actually be anything. As long as it matches what you use to retrieve the data, and is in the list returned by the mimeTypes method of the model. To pass the object itself, you must first serialize it (you can convert your object to xml alternatively, if that was something you are planning on doing), since it is not a standard type for mime data.
In order for the data you enter to be saved you must re-implement the setData method of the model and define behaviour for EditRole.
The relevant methods:
def setData(self, index, value, role):
if role == Qt.EditRole:
self.nodeFromIndex(index).name = value
self.dataChanged.emit(index, index)
return True
def mimeTypes(self):
return ['bstream', 'text/xml']
def mimeData(self, indexes):
mimedata = QtCore.QMimeData()
# assuming single dragged item ...
# only pass the node name
# mimedata.setData('text/xml', str(self.nodeFromIndex(indexes[0]).name))
# pass the entire object
bstream = cPickle.dumps(self.nodeFromIndex(indexes[0]))
mimedata.setData('bstream', bstream)
return mimedata
def dropMimeData(self, mimedata, action, row, column, parentIndex):
if action == Qt.IgnoreAction: return True
parentNode = self.nodeFromIndex(parentIndex)
# data = mimedata.data('text/xml')
data = cPickle.loads(str(mimedata.data('bstream')))
print '\n\t incoming row number:', row, ', incoming column:', column, \
', action:', action, ' mimedata: ', data.name
print "\n\t Item's name on which drop occurred: ", parentNode.name, \
', number of its childred:', len(parentNode.children)
if len(parentNode.children)>0: print '\n\t zero indexed child:', parentNode.children[0].name
return True
EDIT:
That is a lot of code you updated, but I will oblige on the points you highlighted. Avoid calling createIndex outside of the model class. This is a protected method in Qt; Python doesn't enforce private/protected variables or methods, but when using a library from another language that does, I try to respect the intended organization of the classes, and access to them.
The purpose of the model is to provide an interface to your data. You should access it using the index, data, parent etc. public functions of the model. To get the parent of a given index, use that index's (or the model's) parent function, which will also return a QModelIndex. This way, you don't have to go through (or indeed know about) the internal structure of the data. This is what I did in the deleteLevel method.
From the qt docs:
To ensure that the representation of the data is kept separate from the way it is accessed, the concept of a model index is introduced. Each piece of information that can be obtained via a model is represented by a model index... only the model needs to know how to obtain data, and the type of data managed by the model can be defined fairly generally.
Also, you can use recursion to simplify the print method.
def printOut(self):
result_dict = dictify(self.treeView.model().root)
self.make_dirs_from_dict(result_dict)
def deleteLevel(self):
if len(self.treeView.selectedIndexes()) == 0:
return
currentIndex = self.treeView.selectedIndexes()[0]
self.treeView.model().removeRow(currentIndex.row(), currentIndex.parent())
I had this separate from class
def dictify(node):
kids = {}
for child in node.children:
kids.update(dictify(child))
return {str(node.name): kids}

python print contents from wx.ListCtrl

I have a list created as
self.statusListCtrl = wx.ListCtrl(self.panelUpper, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER)
I add data to this list using
self.statusListCtrl.Append([datetime.datetime.now(),action,result])
When my process is all done I have a nice list showing things that were tried, the result of that attempt and a datetime stamp. Now what I want to do is output that to a text file. my problem is that I cant get the data from my listctrl correctly.
This is how I am trying to iterate through the list.
fileName = 'd:\ssplogs\sspInstaller.log'
FILE = open(fileName,"w")
for itemIdx in range(0,self.statusListCtrl.GetItemCount()):
line = str(self.statusListCtrl.GetItemText(itemIdx) + "\n")
print "line" , line
writeLine = line
FILE.write(writeLine)
FILE.close()
The output I am getting though is only my datetime stamp, which is the first column of my list. How do I get this so I see something like
datetime, action, result
Use this to get Row x Column data:
self.list.GetItem(row, col).GetText()
Working example:
import wx
class MainWindow(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
self.panel = wx.Panel(self)
self.list = wx.ListCtrl(self.panel, style=wx.LC_REPORT)
self.list.InsertColumn(0, "Date")
self.list.InsertColumn(1, "Action")
self.list.InsertColumn(2, "Result")
for a in range(5):
self.list.Append(["Date %d" % a, "Action %d" % a, "Result %d" % a])
self.sizer = wx.BoxSizer()
self.sizer.Add(self.list, proportion=1, flag=wx.EXPAND)
self.panel.SetSizerAndFit(self.sizer)
self.Show()
# Save
for row in range(self.list.GetItemCount()):
print(", ".join([self.list.GetItem(row, col).GetText() for col in range(self.list.GetColumnCount())]))
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()

Resources