I need to use a custom QtableView in python to display and format data.
The example app below shows a table with in the first column floats formatted as strings to get proper number of decimals, second column are pure float displayed so without formatting and the third one are strings.
When clicking on columns I want to sort my data which works fine for strings and floats (columns #2 and #3) but not for my column #1 with formatted floats as strings where it's sorted alphabetically rather than numerically.
I'm googling since a while without finding a way to have something working with QtableView.
Any clue on how to get both floats sorting and decimal formatting ?
Thanks & cheers
Stephane
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import *
# Table model
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
# Set columns headers
self.horizontalHeaders = [''] * 3
self.setHeaderData(0, Qt.Horizontal, "Col #1\nfloats as string")
self.setHeaderData(1, Qt.Horizontal, "Col #2\nfloats")
self.setHeaderData(2, Qt.Horizontal, "Col #3\nstrings")
def data(self, index, role):
value = self._data[index.row()][index.column()]
if role == Qt.DisplayRole:
# convert col #1 from floats to string to get proper number of decimal formatting
if index.column() == 0:
return '%.4f' % value
# otherwise display floats or strings for col #2 and #3
else:
return value
# Align values right
if role == Qt.TextAlignmentRole:
return Qt.AlignVCenter + Qt.AlignRight
def rowCount(self, index):
# The length of the outer list.
return len(self._data)
def columnCount(self, index):
# The following takes the first sub-list, and returns
# the length (only works if all rows are an equal length)
return len(self._data[0])
def setHeaderData(self, section, orientation, data, role=Qt.EditRole):
if orientation == Qt.Horizontal and role in (Qt.DisplayRole, Qt.EditRole):
try:
self.horizontalHeaders[section] = data
return True
except:
return False
return super().setHeaderData(section, orientation, data, role)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
try:
return self.horizontalHeaders[section]
except:
pass
return super().headerData(section, orientation, role)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Create a TableView (not a TableWidget !!!)
self.table = QtWidgets.QTableView()
# sample data
data = [
[4.2, 9.6, 1],
[42.1, 0.0, 11],
[3.1, 5.55, 2],
[30.0, 3.55, 2222],
[7.99, 8.99, 33],
]
# Set table model
self.model = TableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
# Use proxy for column sorting
proxyModel = QSortFilterProxyModel()
proxyModel.setSourceModel(self.model)
self.table.setModel(proxyModel)
self.table.setSortingEnabled(True)
# hide vertical headers
self.table.verticalHeader().setVisible(False)
# format horizontal headers
stylesheet = "::section{Background-color:rgb(171,178,185);font-weight:bold}"
self.table.setStyleSheet(stylesheet)
self.table.setAlternatingRowColors(True)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.setMinimumSize(350, 250)
window.setWindowTitle('Sorting column example')
window.show()
app.exec_()
Thanks to a colleague I've found an implementation which works.
Basically one has to override the sorting function and not using the QSortFilterProxyModel() function but rewrite your own function
this new sorting function will be called and just do a custom sorting
Here is the modified code which now works fine for any type of data.
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import *
# Table model
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
# Set columns headers
self.horizontalHeaders = [''] * 3
self.setHeaderData(0, Qt.Horizontal, "Col #1\nfloats as string")
self.setHeaderData(1, Qt.Horizontal, "Col #2\nfloats")
self.setHeaderData(2, Qt.Horizontal, "Col #3\nstrings")
def data(self, index, role):
value = self._data[index.row()][index.column()]
if role == Qt.DisplayRole:
# convert col #1 from floats to string to get proper number of decimal formatting
if index.column() == 0:
return '%.4f' % value
# otherwise display floats or strings for col #2 and #3
else:
return value
if role == Qt.UserRole:
return value
# Align values right
if role == Qt.TextAlignmentRole:
return Qt.AlignVCenter + Qt.AlignRight
def rowCount(self, index):
# The length of the outer list.
return len(self._data)
def columnCount(self, index):
# The following takes the first sub-list, and returns
# the length (only works if all rows are an equal length)
return len(self._data[0])
def setHeaderData(self, section, orientation, data, role=Qt.EditRole):
if orientation == Qt.Horizontal and role in (Qt.DisplayRole, Qt.EditRole):
try:
self.horizontalHeaders[section] = data
return True
except:
return False
return super().setHeaderData(section, orientation, data, role)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
try:
return self.horizontalHeaders[section]
except:
pass
return super().headerData(section, orientation, role)
class mysortingproxy(QSortFilterProxyModel):
def __init__(self):
super(mysortingproxy, self).__init__()
def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
leftDqtq = self.sourceModel().data(left, Qt.UserRole)
rightDqtq = self.sourceModel().data(right, Qt.UserRole)
return leftDqtq < rightDqtq
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Create a TableView (not a TableWidget !!!)
self.table = QtWidgets.QTableView()
# sample data
data = [
[4.2, 9.6, 1],
[42.1, 0.0, 11],
[3.1, 5.55, 2],
[30.0, 3.55, 2222],
[7.99, 8.99, 33],
]
# Set table model
self.model = TableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
# Use proxy for column sorting overriding the QSortFilterProxyModel() function with a custom sorting proxy function
proxyModel = mysortingproxy()
proxyModel.setSourceModel(self.model)
self.table.setModel(proxyModel)
self.table.setSortingEnabled(True)
# hide vertical headers
self.table.verticalHeader().setVisible(False)
# format horizontal headers
stylesheet = "::section{Background-color:rgb(171,178,185);font-weight:bold}"
self.table.setStyleSheet(stylesheet)
self.table.setAlternatingRowColors(True)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.setMinimumSize(350, 250)
window.setWindowTitle('Sorting column example')
window.show()
app.exec_()
Related
I am trying to use pystray to create a icon on tasktray, it is working on windows but now I am building one for Mac. I need the program minimize to tasktray on run on background. so I need to use icon.run_detached() instead of icon.run().
However, it keep crashing the app and I read the documents seems that I need to give some darwin_nsapplication = AppKit.NSApplication.sharedApplication() to the code but I really don't know how to implement this. here is my code.
import tkinter as tk
import time
import pystray
from tkinter import *
from tkinter import messagebox
from PIL import Image
import AppKit
`class Gui():
def __init__(self):
self.window = tk.Tk()
self.darwin_nsapplication = AppKit.NSApplication.sharedApplication()
self.image = Image.open("./images/noname.png")
self.menu = (
pystray.MenuItem('Show', self.show_window),
pystray.MenuItem('Quit', self.quit_window)
)
# Declaration of variables
self.hour=StringVar()
self.minute=StringVar()
self.second=StringVar()
# setting the default value as 0
self.hour.set("00")
self.minute.set("00")
self.second.set("00")
# Use of Entry class to take input from the user
hourEntry= Entry(self.window, width=3, font=("Arial",18,""),
textvariable=self.hour)
hourEntry.place(x=80,y=20)
minuteEntry= Entry(self.window, width=3, font=("Arial",18,""),
textvariable=self.minute)
minuteEntry.place(x=130,y=20)
secondEntry= Entry(self.window, width=3, font=("Arial",18,""),
textvariable=self.second)
secondEntry.place(x=180,y=20)
# button widget
btn = Button(self.window, text='Set Time Countdown', bd='5',
command= self.submit)
btn.place(x = 70,y = 120)
def submit(self):
try:
# the input provided by the user is
# stored in here :temp
temp = int(self.hour.get())*3600 + int(self.minute.get())*60 + int(self.second.get())
except:
print("Please input the right value")
while temp >-1:
# divmod(firstvalue = temp//60, secondvalue = temp%60)
mins,secs = divmod(temp,60)
# Converting the input entered in mins or secs to hours,
# mins ,secs(input = 110 min --> 120*60 = 6600 => 1hr :
# 50min: 0sec)
hours=0
if mins >60:
# divmod(firstvalue = temp//60, secondvalue
# = temp%60)
hours, mins = divmod(mins, 60)
# using format () method to store the value up to
# two decimal places
self.hour.set("{0:2d}".format(hours))
self.minute.set("{0:2d}".format(mins))
self.second.set("{0:2d}".format(secs))
# updating the GUI window after decrementing the
# temp value every time
self.window.update()
time.sleep(1)
# when temp value = 0; then a messagebox pop's up
# with a message:"Time's up"
if (temp == 0):
messagebox.showinfo("Time Countdown", "Time's up ")
# after every one sec the value of temp will be decremented
# by one
temp -= 1
def quit_window(self):
self.icon.stop()
self.window.destroy()
def show_window(self):
self.icon.stop()
self.window.protocol('WM_DELETE_WINDOW', self.withdraw_window)
self.window.after(0, self.window.deiconify)
def withdraw_window(self):
self.window.withdraw()
self.icon = pystray.Icon("name", self.image, "title", self.menu)
self.icon.run_detached()
if __name__ in '__main__':
app = Gui()
app.window.protocol('WM_DELETE_WINDOW', app.withdraw_window)
app.window.mainloop()`
I tried to add darwin_nsapplication to icon like self.icon = pystray.Icon("name", self.image, "title", self.menu,self.darwin_nsapplication)
But it is said 6 arguments are given, 2-5 are needed.
I want to recreate the training dataset by a function generate_custom_train_set at the beginning of every epoch, however, is there a way I could do it with Trainer using callback?
My trainer looks like
trainer = Trainer(
model=model,
args=args,
train_dataset=train_dataset.,
eval_dataset=validation_dataset,
tokenizer=tokenizer,
)
I'm having the same question as I try to implement Examples-proportional mixing from the T5 paper. I didn't find support from hugging face.
My current solution is to modify the trainer.train_dataset in the on_epoch_begin callback.
Here's an implementation. I'm using this in my own project. Seems to work.
First, implement your per-epoch change in your Dataset, in my case, it's the sample function for Examples-Proportional Mixing.
class ProportionMixingDataset:
"""
Examples-proportional mixing from T5
TODO: failed to find a pytorch working implementation
Equivalent to, for the larger datasets, a new subset is taken at each epoch,
then sample in the joined subset once
"""
def __init__(self, dataset_list: List[Dataset] = None, k: int = None):
"""
:param dataset_list: Ordered list of datasets
:param k: Artificial limit
"""
self.dsets = dataset_list
assert k is not None
self.k = k
self.dset_szs = [min(len(d), k) for d in self.dsets]
self.sz = sum(self.dset_szs)
self._sampled_idxs: List[Optional[torch.Tensor]] = [None] * len(self.dsets)
self.sample()
def sample(self):
"""
Sub-sample datasets larger than k
Intended to call in each epoch
"""
for i, dset in enumerate(self.dsets):
sz = len(dset)
if sz > self.k:
self._sampled_idxs[i] = torch.randperm(sz)[:self.k]
def __len__(self):
return self.sz
def _idx2dset_idx(self, idx: int) -> Tuple[int, int]:
"""
Convert a global index to a dataset index
"""
for i, sz in enumerate(self.dset_szs):
if idx < sz:
return i, idx
idx -= sz
raise ValueError('Should not happen')
def __getitem__(self, idx):
if not isinstance(idx, int):
raise ValueError('Batched indexing not supported')
idx_dset, idx = self._idx2dset_idx(idx)
dset = self.dsets[idx_dset]
if self._sampled_idxs[idx_dset] is not None: # A sub-sample index
idx = self._sampled_idxs[idx_dset][idx].item()
return dset[idx]
Then pass that dataset to Trainer.
Now comes the magic part:
class ProportionalMixCallback(TrainerCallback):
"""
Trigger re-computing subset for dataset Examples-proportional mixing, see `dataset::ProportionMixingDataset`
A hack that modifies the train dataset, pointed by Trainer's dataloader
"""
def __init__(self, trainer: Trainer):
self.trainer = trainer
def on_epoch_begin(self, args: TrainingArguments, state, control, **kwargs):
self.trainer.train_dataset.sample()
Pass this to your trainer as a callback.
This triggers the sample call which modifies the dataset at the times we need it.
This works becasue train_dataLoader in trainer still points to the same train dataset object.
Doing things on Google Colab.
transformers: 4.10.2
pytorch-lightning: 1.2.7
import torch
from torch.utils.data import DataLoader
from transformers import BertJapaneseTokenizer, BertForSequenceClassification
import pytorch_lightning as pl
dataset_for_loader = [
{'data':torch.tensor([0,1]), 'labels':torch.tensor(0)},
{'data':torch.tensor([2,3]), 'labels':torch.tensor(1)},
{'data':torch.tensor([4,5]), 'labels':torch.tensor(2)},
{'data':torch.tensor([6,7]), 'labels':torch.tensor(3)},
]
loader = DataLoader(dataset_for_loader, batch_size=2)
for idx, batch in enumerate(loader):
print(f'# batch {idx}')
print(batch)
category_list = [
'dokujo-tsushin',
'it-life-hack',
'kaden-channel',
'livedoor-homme',
'movie-enter',
'peachy',
'smax',
'sports-watch',
'topic-news'
]
tokenizer = BertJapaneseTokenizer.from_pretrained(MODEL_NAME)
max_length = 128
dataset_for_loader = []
for label, category in enumerate(tqdm(category_list)):
# file ./text has lots of articles, categorized by category
# and they are just plain texts, whose content begins from forth line
for file in glob.glob(f'./text/{category}/{category}*'):
lines = open(file).read().splitlines()
text = '\n'.join(lines[3:])
encoding = tokenizer(
text,
max_length=max_length,
padding='max_length',
truncation=True
)
encoding['labels'] = label
encoding = { k: torch.tensor(v) for k, v in encoding.items() }
dataset_for_loader.append(encoding)
SEED=lambda:0.0
# random.shuffle(dataset_for_loader) # ランダムにシャッフル
random.shuffle(dataset_for_loader,SEED)
n = len(dataset_for_loader)
n_train = int(0.6*n)
n_val = int(0.2*n)
dataset_train = dataset_for_loader[:n_train]
dataset_val = dataset_for_loader[n_train:n_train+n_val]
dataset_test = dataset_for_loader[n_train+n_val:]
dataloader_train = DataLoader(
dataset_train, batch_size=32, shuffle=True
)
dataloader_val = DataLoader(dataset_val, batch_size=256)
dataloader_test = DataLoader(dataset_test, batch_size=256)
class BertForSequenceClassification_pl(pl.LightningModule):
def __init__(self, model_name, num_labels, lr):
super().__init__()
self.save_hyperparameters()
self.bert_sc = BertForSequenceClassification.from_pretrained(
model_name,
num_labels=num_labels
)
def training_step(self, batch, batch_idx):
output = self.bert_sc(**batch)
loss = output.loss
self.log('train_loss', loss)
return loss
def validation_step(self, batch, batch_idx):
output = self.bert_sc(**batch)
val_loss = output.loss
self.log('val_loss', val_loss)
def test_step(self, batch, batch_idx):
labels = batch.pop('labels')
output = self.bert_sc(**batch)
labels_predicted = output.logits.argmax(-1)
num_correct = ( labels_predicted == labels ).sum().item()
accuracy = num_correct/labels.size(0)
self.log('accuracy', accuracy)
def configure_optimizers(self):
return torch.optim.Adam(self.parameters(), lr=self.hparams.lr)
checkpoint = pl.callbacks.ModelCheckpoint(
monitor='val_loss',
mode='min',
save_top_k=1,
save_weights_only=True,
dirpath='model/',
)
trainer = pl.Trainer(
gpus=1,
max_epochs=10,
callbacks = [checkpoint]
)
model = BertForSequenceClassification_pl(
MODEL_NAME, num_labels=9, lr=1e-5
)
### (a) ###
# I think this is where I am doing fine-tuning
trainer.fit(model, dataloader_train, dataloader_val)
# this is to score after fine-tuning
test = trainer.test(test_dataloaders=dataloader_test)
print(f'Accuracy: {test[0]["accuracy"]:.2f}')
But I am not really sure how to do a test before fine-tuning, in order to compare two models before and after fine-tuning, in order to show how effective fine-tuning is.
Inserting the following two lines to ### (a) ###:
test = trainer.test(test_dataloaders=dataloader_test)
print(f'Accuracy: {test[0]["accuracy"]:.2f}')
I got this result:
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-13-c8b2c67f2d5c> in <module>()
9
10 # 6-19
---> 11 test = trainer.test(test_dataloaders=dataloader_test)
12 print(f'Accuracy: {test[0]["accuracy"]:.2f}')
13
/usr/local/lib/python3.7/dist-packages/pytorch_lightning/trainer/trainer.py in test(self, model, test_dataloaders, ckpt_path, verbose, datamodule)
896 self.verbose_test = verbose
897
--> 898 self._set_running_stage(RunningStage.TESTING, model or self.lightning_module)
899
900 # If you supply a datamodule you can't supply train_dataloader or val_dataloaders
/usr/local/lib/python3.7/dist-packages/pytorch_lightning/trainer/trainer.py in _set_running_stage(self, stage, model_ref)
563 the trainer and the model
564 """
--> 565 model_ref.running_stage = stage
566 self._running_stage = stage
567
AttributeError: 'NoneType' object has no attribute 'running_stage'
I noticed that Trainer.fit() can take None as arguments other than model, so I tried this:
trainer.fit(model)
test=trainer.test(test_dataloaders=dataloader_test)
print(f'Accuracy: {test[0]["accuracy"]:.2f}')
The result:
MisconfigurationException: No `train_dataloader()` method defined. Lightning `Trainer` expects as minimum a `training_step()`, `train_dataloader()` and `configure_optimizers()` to be defined.
Thanks.
The Trainer needs to call its .fit() in order to set up a lot of things and then only you can do .test() or other methods.
You are right about putting a .fit() just before .test() but the fit call needs to a valid one. You have to feed a dataloader/datamodule to it. But since you don't want to do a training/validation in this fit call, just pass limit_[train/val]_batches=0 while Trainer construction.
trainer = Trainer(gpus=..., ..., limit_train_batches=0, limit_val_batches=0)
trainer.fit(model, dataloader_train, dataloader_val)
trainer.test(model, dataloader_test) # without fine-tuning
The fit call here will just set things up for you and skip training/validation. And then the testing follows. Next time run the same code but without the limit_[train/val]_batches, this will do the pretraining for you
trainer = Trainer(gpus=..., ...)
trainer.fit(model, dataloader_train, dataloader_val)
trainer.test(model, dataloader_test) # with fine-tuning
Clarifying a bit about .fit() taking None for all but model: Its not quite true - you must provide either a DataLoader or a DataModule.
I have this code, which when i run it in PyQt it works totally fine, but when i run it in pyside things get wierd. I get all the columns and rows im supposed to, and if i go to them via scripting and get the data, each cell says what it should. However, even though i set these as display roles, NO text shows in the table.
None in the headers, none in any of the cells. Im at a loss!
(For thos wondering, NulLVariant() just returns either None or QVariant() depending if were on pyside or pyqt)
This model is meant to take a List of Dicts to addRows, and uses dict keys to make columns.
class CustomTableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None, parentTable=None):
"""
Custom data model for holding table data.
:param parent: The parent widget/layout so that this data model gets deleted properly on close.
:param parentTable: the table that is using this data. This is used to get the font metrics of the table
display font.
"""
super(CustomTableModel, self).__init__(parent)
self.parent_table = parentTable
self.auto_resize = False
self._avg_font_w = 5
self._resize_data = defaultdict(int)
self.items = []
self.headers = []
def setParentTable(self, widget):
"""
Sets the parent table widget so that we can get its font metrics for setting our column width with autoResize.
:param widget: TableViewWidget
:raise TypeError:
"""
if not isinstance(widget, QtGui.QTableView):
raise TypeError('Must be a TableView item')
self.parent_table = widget
def setAutoResize(self, b):
"""
Turns on or off auto resize for the table. This gathers the font metrics of the parent table, and then loops
over any current data, or newly added data (including table headers) to get the widest item, and sets the
column width to fit this.
:param b: bool
:raise AttributeError:
"""
if not self.parent_table:
raise AttributeError('You must call setParentTable first to set the parent TableView item')
self.auto_resize = b
if b:
self._autoAllResizeData()
self._doColumnResize()
else:
self._resize_data = dict()
def updateSize(self):
"""
Force the table size to update to the current size data.
"""
self._doColumnResize()
def updateSizeData(self):
"""
Force an update/regathering of all the size data for each row and column.
"""
self._autoAllResizeData(True)
self._doColumnResize()
def _doColumnResize(self):
for i in range(len(self.headers)):
txt = self.headers[i]
self.parent_table.setColumnWidth(i, self._resize_data.get(txt))
def _getKeyList(self):
if self.headers:
return self.headers
elif self.items:
return sorted(self.items[0].keys())
def _getTableFontWidth(self):
self._avg_font_w = self.parent_table.fontMetrics().averageCharWidth()
def _autoAllResizeData(self, reset=False):
if not self._resize_data or reset is True:
self._resize_data = defaultdict(int)
key_list = self._getKeyList()
for header in key_list:
header_width = len(header) * (self._avg_font_w * 1.55)
if header_width > self._resize_data[header]:
self._resize_data[header] = header_width
for item in self.items:
value = item.get(header)
width = len(str(value)) * self._avg_font_w
if width > self._resize_data[header]:
self._resize_data[header] = width
def _autoSingleResizeData(self, data):
key_list = self._getKeyList()
for header in key_list:
value = data.get(header)
if value:
width = len(str(value)) * self._avg_font_w
if width > self._resize_data[header]:
self._resize_data[header] = width
def setHeaders(self, items):
"""
This allows you to set your header item text
:param items: a list of header text, ie ['Name', 'Email', 'Department']
"""
lastCount = self.columnCount(QtCore.QModelIndex())
self.headers = items
self.beginRemoveColumns(QtCore.QModelIndex(), 0, lastCount)
for x in range(lastCount):
self.removeColumn(x)
self.endRemoveColumns()
self.beginInsertColumns(QtCore.QModelIndex(), 0, len(items)-1)
self.endInsertColumns()
def addRow(self, data):
"""
Accepts a dict of data to add to the data model.
:param data: dict (this should match the same key length/names as the other data in the table.)
"""
row = len(self.items)
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.items.append(data)
self.endInsertRows()
if self.auto_resize:
self._autoSingleResizeData(data)
self._doColumnResize()
def addRows(self, data):
"""
Accepts a list of dicts to add them all to the table, with each list index being a row, and each dict key
a column.
:param data: list of dicts
:raise ValueError:
"""
if not isinstance(data, list) or not isinstance(data[0], dict):
raise ValueError('input must be a list of dicts!')
start_row = len(self.items)
end_row = len(data) + start_row - 1
self.beginInsertRows(QtCore.QModelIndex(), start_row, end_row)
self.items.extend(data)
self.endInsertRows()
if self.auto_resize:
for item in data:
self._autoSingleResizeData(item)
self._doColumnResize()
def removeRow(self, row):
"""
Remove the row at index 'row'.
:param row: int
"""
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.items.pop(row)
self.endRemoveRows()
def clear(self):
"""
Clear all table data and start fresh.
"""
rows = self.rowCount(QtCore.QModelIndex())
self.beginRemoveRows(QtCore.QModelIndex(), 0, rows)
self.items = []
self.endRemoveRows()
cols = self.columnCount(QtCore.QModelIndex())
self.beginRemoveColumns(QtCore.QModelIndex(), 0, cols)
self.headers = []
self.endRemoveColumns()
def rowCount(self, QModelIndex):
"""
Return the row count.
:param QModelIndex:
:return:
"""
return len(self.items)
def columnCount(self, QModelIndex):
"""
Return the column count (default 1)
:param QModelIndex:
:return:
"""
try:
return len(self.items[0].keys())
except:
return 1
def data(self, index, role):
"""
Accepts a QModelIndex and a Qt.Role and returns the data at the given modelIndex.
:param index: QModelIndex
:param role: QtCore.Qt.<Role>
:return:
"""
row = index.row()
col = index.column()
if role == QtCore.Qt.DisplayRole:
key_list = self._getKeyList()
return QtCore.QVariant(str(self.items[row][key_list[col]]))
return NullVariant()
def intGetData(self, row, col):
"""
Gets the data at 'row' and 'col'.
:param row: int
:param col: int
:return: QVariant() data.
"""
try:
key_list = self._getKeyList()
return QtCore.QVariant(str(self.items[row][key_list[col]]))
except:
return NullVariant()
def headerData(self, section, orientation, role):
"""
Sets the header data based on our header key list.
:param section: section header
:param orientation: orientation
:param role: Qt<Role>
:return:
"""
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if not self.items:
if section == 0:
return QtCore.QVariant(str("Column 1"))
else:
key_list = self._getKeyList()
try:
return QtCore.QVariant(str(key_list[section]))
except:
return QtCore.QVariant('No Data')
return NullVariant()
class CustomSortModel(QtGui.QSortFilterProxyModel):
def __init__(self, parent=None):
"""
Custom QSortFilterProxyModel to allow sorting and filtering of our custom data model.
:param parent: parent so that this model is deleted properly upon close.
"""
super(CustomSortModel, self).__init__(parent)
self.countAllColumns = False
self._sortingColumn = 0
def filterAcceptsRow(self, sourceRow, sourceParent):
"""
Overriding how we choose what rows match our input filter text.
:param sourceRow: row index in question
:param sourceParent: QModelIndex
:return: bool (accepted or not)
"""
txt = ''
if self.countAllColumns:
for x in range(len(self.sourceModel().headers)):
txt += self.sourceModel().intGetData(sourceRow, x).toString()
else:
txt = self.sourceModel().intGetData(sourceRow, self._sortingColumn).toString()
if self.filterRegExp().pattern():
b = bool(re.search(str(self.filterRegExp().pattern()), str(txt)))
else:
b = bool(re.search('.*', str(txt)))
return b
def setFilterKeyColumn(self, col):
"""
Sets which column index you want the filter to apply to. -1 or less means we search all columns - otherwise,
the filter rules apply to the column index given.
:param col: signed int
:return:
"""
if col <= -1:
self.countAllColumns = True
return
self.countAllColumns = False
self._sortingColumn = col
super(CustomSortModel, self).setFilterKeyColumn(col)
Edit:
I was getting a wierd error when i tried to delete this question, but I have added a newer one, with a better cut down example for testing here:
https://stackoverflow.com/questions/34074825/pyside-qtableview-not-displaying-text-like-pyqt-does
Running your code in PySide gives a bunch of errors :
AttributeError: 'module' object has no attribute 'QVariant'
That's because there is no QVariant in PySide any more. Replacing all QVariantby regular python types fixes the code.
For example
return QtCore.QVariant('No Data')
becomes
return "No Data"
I have some data which are represented by a class of my own ; to fix the ideas I give an example.
class MyOwnModel():
def __init__(self, name="", number=0):
self.name = name
self.number = number
I then have a list of such instances, that I want to represent in a QTableView.
li = [MyOwnModel("a", 1), MyOwnModel("b", 2)]
Then I see two strategies to make a QTableView from that :
change MyOwnModel so that it subclasses QAbstractTableModel
build a new QAbstractTableModel which mimics MyOwnModel in a way that its attributes are for instance two QString and connect the dataChanged signal to a function which updates the instance of MyOwnModel
I am not completely satisfied with any of these, but I have no other idea for the moment.
Which one is the most suitable to my problem ? (I have a more complex class in practice but I would like to use the same framework)
As stated in the comment, your model is your list of object. You should subclass QAbstractTableModel to use this list.
Here's my code snippet for this:
import sys
import signal
import PyQt4.QtCore as PCore
import PyQt4.QtGui as PGui
class OneRow(PCore.QObject):
def __init__(self):
self.column0="text in column 0"
self.column1="text in column 1"
class TableModel(PCore.QAbstractTableModel):
def __init__(self):
super(TableModel,self).__init__()
self.myList=[]
def addRow(self,rowObject):
row=len(self.myList)
self.beginInsertRows(PCore.QModelIndex(),row,row)
self.myList.append(rowObject)
self.endInsertRows()
#number of row
def rowCount(self,QModelIndex):
return len(self.myList)
#number of columns
def columnCount(self,QModelIndex):
return 2
#Define what do you print in the cells
def data(self,index,role):
row=index.row()
col=index.column()
if role==PCore.Qt.DisplayRole:
if col==0:
return str( self.myList[row].column0)
if col==1:
return str( self.myList[row].column1)
#Rename the columns
def headerData(self,section,orientation,role):
if role==PCore.Qt.DisplayRole:
if orientation==PCore.Qt.Horizontal:
if section==0:
return str("Column 1")
elif section==1:
return str("Column 2")
if __name__=='__main__':
PGui.QApplication.setStyle("plastique")
app=PGui.QApplication(sys.argv)
#Model
model=TableModel()
model.addRow(OneRow())
model.addRow(OneRow())
#View
win=PGui.QTableView()
win.setModel(model)
#to be able to close wth ctrl+c
signal.signal(signal.SIGINT, signal.SIG_DFL)
#to avoid warning when closing
win.setAttribute(PCore.Qt.WA_DeleteOnClose)
win.show()
sys.exit(app.exec_())
Each element of myList is a row in the table.