I am writing a tool that allows me to track some tasks along a path of predifined stages, from something on a backlog, to ToDo, through WIP, Review and finally to done.
I created a custom widget, that will eventually be yellow, not unlike a postit note and perhaps with a bit of formatting it to give it a nice frame, etc... but stopped before getting far enough to make it look right because of this issue.
The idea is that each of these yellow Task widgets will have a stage they are at, and that I can select them in a Table Widget, and move them onto the next or previous stage, which will update taht objects stage, then refresh the TableWidget, read all the widget and where thay should be and set them in their new place.
So I have it kind of working to some degree (below), where I can move the tasks forward and they update location, but I noticed when I click the cells that the widget was previously in, print statement still says that the cell still has a widget there (which kind of makes sense, as code below isn't removing the previous one, but I'd expect to visually still see it). And I can move them forward and backwards, and the information on the tasks does update correctly, but the table won't refresh unless the task moves to a cell that never had a cellWidget in it. Test this by moving it backwards. It works, movnig forward visually does nothing, but moving again, does show up.
I tried clearing the TableWidget and rebuilding from scratch and that crashes. The main issue I am having is that with all these crashes, which is an issue in itself as it makes debugging very tough... When I try and clear the TableWidget (with .clear()) before repopulating, I get this.
Process finished with exit code -1073741819 (0xC0000005)
Same error code if I try removing the old cells by setting the Table Widget to 0 rows before adding the correct number of rows.
A known issue that is less important is when I select a cell without a widget and try and move it, gies me this, but don't worry too much about that fix, as it's known issue.
Process finished with exit code -1073740791 (0xC0000409)
Also tried cleaning up by iterating every cell and if it has a cell widget, remove cell widget before re-setting them to correct place and it still crashes. I'm out of ideas.
Task Widget
import sys
from PyQt5.QtWidgets import (QApplication, QTableWidget, QWidget, QFrame, QHBoxLayout, QLabel,
QPushButton,QVBoxLayout)
class Task(QWidget):
def __init__(self, ID, name, est):
super(Task, self).__init__()
# Creates a small widget that will be added to a table widget
self.ID = ID
self.name = name
self.est = est
# These cell widgets represent tasks. So each task has a particular 'stage' it is at
self.stage = 'ToDo'
self.stages = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
self.objects_labels = {}
self.initUI()
def initUI(self):
# adds a bunch of labels to the widget
layout = QVBoxLayout()
frame = QFrame()
frame.setFrameShape(QFrame.StyledPanel)
frame.setStyleSheet('background-color: red')
frame.setLineWidth(2)
layout.addWidget(frame)
info = [self.ID, self.name, self.est]
for section in info:
self.objects_labels[section] = QLabel(str(section))
layout.addWidget(self.objects_labels[section])
self.setLayout(layout)
self.setStyleSheet('background-color: yellow')
def task_move(self, forward = True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
print(self.stages)
print(index)
if forward:
print('--->')
if self.stage == self.stages[-1]:
print('Already at the end of process')
return
self.stage = self.stages[index + 1]
else:
print('<---')
if self.stage == self.stages[0]:
print('Already at the start of process')
return
self.stage = self.stages[index - 1]
MainWidget
class MainWidget(QWidget):
def __init__(self):
super().__init__()
self.tasks = self.make_tasks()
self.init_ui()
self.update_tw()
def make_tasks(self):
# Create a few tasks
a = Task(0, 'Name_A', 44)
b = Task(0, 'Name_B', 22)
c = Task(0, 'Name_C', 66)
d = Task(0, 'Name_D', 90)
return [a, b, c, d]
def init_ui(self):
layout_main = QVBoxLayout()
self.tw = QTableWidget()
self.tw.cellClicked.connect(self.cell_clicked)
self.tw.horizontalHeader().setDefaultSectionSize(120)
self.tw.verticalHeader().setDefaultSectionSize(120)
layout_main.addWidget(self.tw)
layout_bottom_button_bar = QHBoxLayout()
self.btn_task_backward = QPushButton('<--- Task')
self.btn_task_backward.clicked.connect(lambda: self.move_task(forward=False))
self.btn_task_forward = QPushButton('Task --->')
self.btn_task_forward.clicked.connect(lambda: self.move_task())
for widget in [self.btn_task_backward, self.btn_task_forward]:
layout_bottom_button_bar.addWidget(widget)
layout_main.addLayout(layout_bottom_button_bar)
self.setLayout(layout_main)
self.setGeometry(300, 300, 800, 600)
self.setWindowTitle('MainWidget')
self.show()
#property
def tw_header(self):
return {'Backlog': 0, 'ToDo': 1, 'WIP': 2, 'Review': 3, 'Done': 4}
#property
def selected_indices(self):
return [(x.row(), x.column()) for x in self.tw.selectedIndexes()]
#property
def selected_widgets(self):
selected_widgets = [self.tw.cellWidget(x[0], x[1]) for x in self.selected_indices]
print(selected_widgets)
return selected_widgets
def move_task(self, forward=True):
# Crashes if you select a non-widget cell, but thats a known issue
# Moves the task forward or backward and then prompts to update the TableWidget
for object in self.selected_widgets:
object.task_move(forward=forward)
self.tw.clearSelection()
self.update_tw()
def cell_clicked(self, row, column):
if self.tw.cellWidget(row, column):
print(self.selected_indices)
print(self.selected_widgets)
else:
print('No Cell Widget here')
def update_tw(self):
#I wanted to clear the Table widget and rebuild, but this crashes
# self.tw.clear()
self.tw.setHorizontalHeaderLabels(self.tw_header.keys())
rows = len(self.tasks)
columns = len(self.tw_header)
self.tw.setRowCount(rows)
self.tw.setColumnCount(columns)
# Looks through each task, and then gets it's stage, and then adds the widget to the correct column
for index, object in enumerate(self.tasks):
column = self.tw_header[object.stage]
print('Setting stage {} for {}\n...to r={}, c={}\n***'.format(object.stage, object, index, column))
self.tw.setCellWidget(index, column, object)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWidget()
sys.exit(app.exec_())
From my previous experience, I always found using setCellWidget clunky, underperforming and buggy.
Most of the times my Widgets were lost or misplaced, while refreshing the table similarly to the way you are doing it.
In addition, I guess you would want to use this "Task Mover" on a larger scale, and from what I could see, setting separate Widgets inside QWidgetItems becomes quite slow when done on loads of items.
My suggestion would be to use style delegates, so that you can customize the look of your items to your liking, without having to deal with the setCellWidget stuff which is giving you problem.
Once you have your own delegate, and paint the items the way you want, you can just keep updating that item data and moving the items around the table by using "take" and "set".
I am not sure if this would be the best way of executing this specific task, but moving towards this direction would probably give you greater flexibility and customisation power in the long run.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class TaskProperty():
properties = ["ID", "name", "est", "stage"]
count = 4
ID, Name, Est, Stage = [Qt.UserRole + x for x in range(count)]
STAGES = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
class MainWidget(QWidget):
def __init__(self):
super(MainWidget, self).__init__()
self.tasks = self.make_tasks()
self.init_ui()
self.update_tw()
def make_tasks(self):
# Create a few tasks
a = Task(0, 'Name_A', 44)
b = Task(0, 'Name_B', 22)
c = Task(0, 'Name_C', 66)
d = Task(0, 'Name_D', 90)
return [a, b, c, d]
def init_ui(self):
layout_main = QVBoxLayout()
self.tw = QTableWidget()
# create and set the delegate to the TableWidget
self.delegate = TaskDelegate(self.tw )
self.tw.setItemDelegate(self.delegate)
self.tw.cellClicked.connect(self.cell_clicked)
self.tw.horizontalHeader().setDefaultSectionSize(120)
self.tw.verticalHeader().setDefaultSectionSize(120)
layout_main.addWidget(self.tw)
layout_bottom_button_bar = QHBoxLayout()
self.btn_task_backward = QPushButton('<--- Task')
self.btn_task_backward.clicked.connect(lambda: self.move_task(forward=False))
self.btn_task_forward = QPushButton('Task --->')
self.btn_task_forward.clicked.connect(lambda: self.move_task())
for widget in [self.btn_task_backward, self.btn_task_forward]:
layout_bottom_button_bar.addWidget(widget)
layout_main.addLayout(layout_bottom_button_bar)
self.setLayout(layout_main)
self.setGeometry(300, 300, 800, 600)
self.setWindowTitle('MainWidget')
self.show()
#property
def tw_header(self):
return {'Backlog': 0, 'ToDo': 1, 'WIP': 2, 'Review': 3, 'Done': 4}
#property
def selected_indices(self):
return [(x.row(), x.column()) for x in self.tw.selectedIndexes()]
def move_task(self, forward=True):
'''
To move the task to the next step, we iterate all the items selected.
If the task can be moved, we take the corresponding item from its current cell and move it to the destination.
:param forward:
:return:
'''
selected =self.tw.selectedItems()
for item in selected:
item.setSelected(False)
result = item.task_move(forward=forward)
if result:
next = 1 if forward else -1
row = item.row()
column = item.column()
moveItem = self.tw.takeItem(row, column)
self.tw.setItem(row, column + next, moveItem)
moveItem.setSelected(True)
def cell_clicked(self, row, column):
item = self.tw.item(row, column)
if not isinstance(item, TaskItem):
print "No Task Item Here"
def update_tw(self):
# I wanted to clear the Table widget and rebuild, but this crashes
# self.tw.clear()
self.tw.clear()
self.tw.setHorizontalHeaderLabels(self.tw_header.keys())
rows = len(self.tasks)
columns = len(self.tw_header)
self.tw.setRowCount(rows)
self.tw.setColumnCount(columns)
# Looks through each task, and then gets it's stage, and then adds the widget to the correct column
for row, object in enumerate(self.tasks):
# create items of our custom type only for the column that need to be filled.
# the other cells will be filled with null items.
column = STAGES.index(object.stage)
print('Setting stage {} for {}\n...to r={}, c={}\n***'.format(object.stage, object, row, column))
item = TaskItem(object)
self.tw.setItem(row, column, item)
class TaskDelegate(QStyledItemDelegate):
'''
This delegate take care of Drawing our cells the way we want it to be.
'''
def paint(self, painter, option, index):
'''
Override the Paint function to draw our own cell.
If the QTableWidgetItem does not have our Data stored in it, we do a default paint
:param painter:
:param option:
:param index:
:return:
'''
painter.save()
rect = option.rect
status = index.data(TaskProperty.Stage)
if status is None:
return super(TaskDelegate, self).paint(painter, option, index)
else:
id = STAGES.index(status)
pen = painter.pen()
pen.setBrush(Qt.black)
painter.setPen(pen)
if id == index.column():
rect.translate(3, 3)
newRect = QRect(rect.x(), rect.y(), rect.width() - 6, 20)
infos = [index.data(TaskProperty.ID), index.data(TaskProperty.Name), index.data(TaskProperty.Est)]
painter.setBrush(Qt.red)
painter.drawRect(newRect)
painter.setBrush(Qt.yellow)
for info in infos:
newRect.translate(0, 25)
painter.drawRect(newRect)
painter.drawText(newRect, Qt.AlignHCenter | Qt.AlignVCenter,
str(info))
class TaskItem(QTableWidgetItem):
'''
Subclass QTableWidgetItem.
Probably not needed, since we can set the property when we create the item instead of in the init,
and keep track of which item is attached to which task object using the Column Index of the table.
However, this can be useful if you want to attach more specific procedures to your items
'''
def __init__(self, task):
super(TaskItem, self).__init__()
self._task = task
self.setData(TaskProperty.ID, task.ID)
self.setData(TaskProperty.Name, task.name)
self.setData(TaskProperty.Est, task.est)
self.setData(TaskProperty.Stage, task.stage)
self.objects_labels = {}
def task_move(self, forward=True):
result = self._task.task_move(forward=forward)
self.setData(TaskProperty.Stage, self._task.stage)
return result
class Task(object):
'''
The Task class is now just an object, not a widget.
'''
def __init__(self, ID, name, est):
# Creates a small widget that will be added to a table widget
self.ID = ID
self.name = name
self.est = est
# These cell widgets represent tasks. So each task has a particular 'stage' it is at
self.stage = 'ToDo'
self.stages = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
self.objects_labels = {}
def task_move(self, forward=True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
if forward:
print('--->')
if self.stage == self.stages[-1]:
#print('Already at the end of process')
return False
self.stage = self.stages[index + 1]
else:
print('<---')
if self.stage == self.stages[0]:
#print('Already at the start of process')
return False
self.stage = self.stages[index - 1]
return True
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWidget()
sys.exit(app.exec_())
It is not necessary to clean and create everything again, instead just move the widget for it we must know if it can be moved or not and for that task_move must indicate if the movement is valid or not. Considering the above, the solution is:
def task_move(self, forward=True):
# The main widget will allow me to change the stage of a particular Task
# The idea is that I update the Table widget to show everything in the right place
# This function finds out what stage it is at and increments/decrements by one
index = self.stages.index(self.stage)
print(self.stages)
print(index)
if forward:
print("--->")
if self.stage == self.stages[-1]:
print("Already at the end of process")
return False
self.stage = self.stages[index + 1]
else:
print("<---")
if self.stage == self.stages[0]:
print("Already at the start of process")
return False
self.stage = self.stages[index - 1]
return True
def move_task(self, forward=True):
for row, column in self.selected_indices:
widget = self.tw.cellWidget(row, column)
if isinstance(widget, Task) and widget.task_move(forward):
next_column = column + (1 if forward else -1)
# create new task widget
task = Task(widget.ID, widget.name, widget.est)
# remove all task widget
self.tw.removeCellWidget(row, column)
# move task widget
self.tw.setCellWidget(row, next_column, task)
self.tw.clearSelection()
The crashed is because when using clear you are also removing the Task widget so "self.tasks" has objects deleted from C++ that you should not use.
I'm writing a program with pyqt5 and want to make the QlineEdit show history input by using sqlite to store the inputs. I use a signal to catch the cursor when focusInEvent happens and select the history records at that time, then I put the results into QCompleter so it can pop up in QlineEdit. Now I can make the history inputs show in QlineEdit object but when I click on any value, 1s later, the whole program quit automatically with error, which says "Python has stopped".
class FocusLineEdit(QLineEdit):
ac = pyqtSignal(list)
def __init__(self, parent=None):
super(FocusLineEdit, self).__init__(parent)
self.ac.connect(self.addCompleter)
def focusInEvent(self, event):
rtl = call_history(self.objectName())
self.ac.emit(rtl)
def addCompleter(self, rtl):
self.autoCompleter = QCompleter(rtl)
self.autoCompleter.setCompletionMode(1)
self.setCompleter(self.autoCompleter)
def focusOutEvent(self, event):
pass
It is difficult to analyze where the problem is without you provide an MCVE, so my response implements what you require without taking into account your current code for it must meet the following requirements:
You must have 2 tables: objects and history that are related.
The table objects saves the name of the filtering history, it is similar to the use of the objectName that you use, but in general 2 widgets can access the same history if the connection establishes the same name
In the history table the information of the words associated with the id of the objects table is saved.
In my example I use the following instructions to create them:
CREATE TABLE IF NOT EXISTS objects (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);
CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY AUTOINCREMENT, id_object INTEGER REFERENCES objects (id), word TEXT NOT NULL, UNIQUE (id_object, word));
Also to test it I created a data, if you already have data then you must eliminate the if test: and everything inside.
Finally for a better understanding of my solution I show a QTableView that is changing according to the selection.
from PyQt5 import QtCore, QtGui, QtWidgets, QtSql
def createConnection():
db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
db_path = 'test.db'
db.setDatabaseName(db_path)
if not db.open():
QMessageBox.critical(None, qApp.tr("Cannot open database"),
qApp.tr("Unable to establish a database connection.\n"
"This example needs SQLite support. Please read "
"the Qt SQL driver documentation for information "
"how to build it.\n\n"
"Click Cancel to exit."),
QMessageBox.Cancel)
return False
test = True
if test:
query = QtSql.QSqlQuery()
if not query.exec_('CREATE TABLE IF NOT EXISTS objects (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE);'):
return False
if not query.exec_('CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY AUTOINCREMENT, id_object INTEGER REFERENCES objects (id), word TEXT NOT NULL, UNIQUE (id_object, word));'):
return False
for i in range(3):
query.prepare('INSERT INTO objects (name) VALUES (?)')
query.addBindValue("obj{}".format(i))
if not query.exec_():
print(query.lastError().text())
import requests
import random
word_site = "http://svnweb.freebsd.org/csrg/share/dict/words?view=co&content-type=text/plain"
response = requests.get(word_site)
WORDS = response.content.decode().splitlines()
print(WORDS)
for i in range(3):
for text in random.sample(WORDS, 50):
query.prepare('INSERT INTO history (id_object, word) VALUES (?, ?)')
query.addBindValue(i+1)
query.addBindValue(text)
if not query.exec_():
print(query.lastError().text())
return True
class Completer(QtWidgets.QCompleter):
def __init__(self, parent=None):
super(Completer, self).__init__(parent)
self._last_words = []
def splitPath(self, path):
if path[-1] != ' ':
words = path.split()
self._last_words = words[:-1] if len(words) > 1 else []
return [words[-1]]
else:
QtCore.QTimer.singleShot(0, self.popup().hide)
return []
def pathFromIndex(self, index):
val = super(Completer, self).pathFromIndex(index)
return ' '.join(self._last_words + [val])
class HistoryManager(QtCore.QObject):
nameChanged = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
super(HistoryManager, self).__init__(parent)
model = QtSql.QSqlRelationalTableModel(self)
model.setTable("history")
model.setRelation(1, QtSql.QSqlRelation("objects", "id", "name"))
model.select()
self._proxy = QtCore.QSortFilterProxyModel(self)
self._proxy.setSourceModel(model)
self._proxy.setFilterKeyColumn(1)
# proxy.setFilterFixedString("obj1")
self._widgets = {}
self._completer = Completer(self)
self._completer.setModel(self._proxy)
self._completer.setCompletionColumn(2)
def register_widget(self, widget, objectname):
# TODO
if callable(getattr(widget, "setCompleter")):
widget.installEventFilter(self)
self._widgets[widget] = objectname
return True
return False
def eventFilter(self, obj, event):
if obj in self._widgets:
if event.type() == QtCore.QEvent.FocusIn:
name = self._widgets[obj]
self._proxy.setFilterFixedString(name)
obj.setCompleter(self._completer)
self.nameChanged.emit(name)
elif event.type() == QtCore.QEvent.FocusOut:
obj.setCompleter(None)
self._proxy.setFilterFixedString("")
self.nameChanged.emit("")
return super(HistoryManager, self).eventFilter(obj, event)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._manager = HistoryManager()
model = QtSql.QSqlRelationalTableModel(self)
model.setTable("history")
model.setRelation(1, QtSql.QSqlRelation("objects", "id", "name"))
model.select()
self._proxy = QtCore.QSortFilterProxyModel(self)
self._proxy.setSourceModel(model)
self._proxy.setFilterKeyColumn(1)
tv = QtWidgets.QTableView()
tv.setModel(self._proxy)
vlay = QtWidgets.QVBoxLayout()
for i in range(3):
le = QtWidgets.QLineEdit()
vlay.addWidget(le)
self._manager.register_widget(le, "obj{}".format(i))
vlay.addStretch()
lay = QtWidgets.QHBoxLayout(self)
lay.addWidget(tv, stretch=1)
lay.addLayout(vlay)
self._manager.nameChanged.connect(self._proxy.setFilterFixedString)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
if not createConnection():
sys.exit(-1)
manager = HistoryManager()
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I'm trying to implement a system in PyQt4 where unchecking a checkbox would call function disable_mod and checking it would call enable_mod. But even though state is changing the checkboxes call the initial function they started with. For this case if an already checked box was clicked it'd always keep calling the disable_mod function! I don't understand why is this happening? Can you guys help me out here a little bit? Here's my code:
from PyQt4 import QtCore, QtGui
from os import walk
from os.path import join
import sys
def list_files_regex(dir):
l = []
for (root, dirnames, filenames) in walk(dir):
for d in dirnames:
list_files_regex(join(root, d))
l.extend(filenames)
return l
directory = "J:/test_pack"
directory = join(directory, "/mods")
count = 0
for y in list_files_regex(directory):
print y
count += 1
print count
class ModEdit(QtGui.QMainWindow):
def __init__(self, title, icon, x, y, w, h):
super(ModEdit, self).__init__()
self.setWindowTitle(title)
self.setWindowIcon(QtGui.QIcon(icon))
self.setGeometry(x, y, w, h)
self.choices = []
self.init()
def init(self):
scroll_widget = QtGui.QScrollArea()
sub_widget = QtGui.QWidget()
v_layout = QtGui.QVBoxLayout()
for y in list_files_regex(directory):
tmp = QtGui.QCheckBox(y, self)
tmp.resize(tmp.sizeHint())
if tmp.text()[len(tmp.text()) - 8: len(tmp.text())] != 'disabled':
tmp.setChecked(True)
# if tmp.isChecked() == 0:
# tmp.stateChanged.connect(self.enable_mod)
# if tmp.isChecked():
# tmp.stateChanged.connect(self.disable_mod)
# v_layout.addWidget(tmp)
self.choices.append(tmp)
print self.choices
for choice in self.choices:
v_layout.addWidget(choice)
if choice.isChecked():
choice.stateChanged.connect(self.disable_mod)
else:
choice.stateChanged.connect(self.enable_mod)
sub_widget.setLayout(v_layout)
scroll_widget.setWidget(sub_widget)
self.setCentralWidget(scroll_widget)
self.show()
def enable_mod(self):
print "ENABLE_TEST"
print self.choices[1].isChecked()
def disable_mod(self):
print "DISABLE_TEST"
print self.choices[1].isChecked()
def test(self):
print 'test'
for ch in self.choices:
if ch.isChecked():
ch.stateChanged.connect(self.disable_mod)
else:
ch.stateChanged.connect(self.enable_mod)
class Rename(QtCore.QObject):
enable = QtCore.pyqtSignal
disable = QtCore.pyqtSignal
app = QtGui.QApplication(sys.argv)
ex = ModEdit("Minecraft ModEdit", "ModEdit.png", 64, 64, 640, 480)
sys.exit(app.exec_())
The problem is that you're only connecting the checkbox stateChanged signal once during initialization. After the state of the checkbox changes, you're not disconnecting the signal and reconnecting it to the correct slot.
You'll need to connect the stateChanged signal to an intermediary slot that will decide which function to call based on the checkbox state. Since you're using the same slot for multiple checkboxes, it's probably best to also pass the checkbox to the slot as well.
from functools import partial
def init(self):
...
for tmp in list_of_checkboxes:
enable_slot = partial(self.enable_mod, tmp)
disable_slot = partial(self.disable_mod, tmp)
tmp.stateChanged.connect(lambda x: enable_slot() if x else disable_slot())
def enable_mod(self, checkbox):
print "ENABLE_TEST"
print checkbox.isChecked()
def disable_mod(self, checkbox):
print "DISABLE_TEST"
print checkbox.isChecked()
Alternatively, since we are now passing the checkbox to the slots, you could just use a single slot and check the checkbox state inside the slot
def init(self):
...
for tmp in list_of_checkboxes:
slot = partial(self.enable_disable_mod, tmp)
tmp.stateChanged.connect(lambda x: slot())
def enable_disable_mod(self, checkbox):
if checkbox.isChecked():
...
else:
...
I need your advice and help for my code.
I am making a GUI program. Basically the program does the followings:
Gets input from the user
When a certain button is pushed, the program retrieves all the input and saved them in the database
The program does computations.
Shows the output
Here is the simple version of the program, calculator program:
As you can see in the figure, it takes 4 input from user, (1) any integer for var1, (2) any integer for var2, (3) operator (addition or subtraction) and (4) click button.
The GUI was designed using QtDesigner.
And here is my code:
import sys
import sqlite3
from PyQt4 import QtCore, QtGui
from testgui import Ui_MainWindow
class ShowAndInput(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#connect to the database
self.connect_to_db()
#create table in the database
self.create_table()
#add items to the comboBox
self.add_items()
#calculate button to start calculating, gives signal to the next class Calculate
self.ui.pushButton_3.clicked.connect(self.calculate)
#close button to close the window
QtCore.QObject.connect(self.ui.pushButton,QtCore.SIGNAL("clicked()"), quit)
self.conn.close()
def connect_to_db(self):
self.conn = sqlite3.connect('testguidb.sqlite3')
self.cur = self.conn.cursor()
def create_table(self):
self.cur.execute('''CREATE TABLE IF NOT EXISTS Input (var1 INTEGER, var2 INTEGER)''')
self.conn.commit()
def add_items(self):
lst = ['addition', 'subtraction']
self.ui.comboBox.clear()
self.ui.comboBox.addItems(lst)
def calculate(self):
calculate = Calculate(self)
calculate.exec_()
So the purpose of the first class was to show the GUI and take input from the user. If user clicks the pushbutton named 'Calculate', it should connect to the new class named 'Calculate'.
Here is my code for the Calculate class:
class Calculate(QtGui.QMainWindow):
def __init__(self, parent = None):
QtGui.QWidget.__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#validator to validate that the input is integer or digit
#connect to database
self.connect_to_db()
#get the input values and store them in the database
self.get_the_values()
#print the output
PrintOutcome()
def connect_to_db(self):
self.conn = sqlite3.connect('testguidb.sqlite3')
self.cur = self.conn.cursor()
def get_the_values(self):
dicti = {}
dicti['var1'] = self.ui.lineEdit.text()
dicti['var2'] = self.ui.lineEdit_2.text()
for key, val in sorted(dicti.items()):
key = str(key)
val = int(val)
self.cur.execute('INSERT OR IGNORE INTO Input (?) VALUES (?)',(key,val))
self.conn.commit()
And at the end of this class, it calls another class named 'PrintOutcome' to print out the result in the QTextBrowser. Here is the last piece of the code:
class PrintOutcome(QtGui.QMainWindow):
def __init__(self, parent = None):
QtGui.QWidget.__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#connect to database
self.connect_to_db()
#get the value of the combobox
self.get_combox()
#print the output
if self.operator == 'addition': self.print_add()
elif self.operator == 'subtraction': self.print_sub()
def connect_to_db(self):
self.conn = sqlite3.connect('testguidb.sqlite3')
self.cur = self.conn.cursor()
def get_combox(self):
self.operator = str(self.ui.comboBox.currentText())
def print_add(self):
dicti={}
for i in range(2):
i = i + 1
dicti['var{0}'.format(i)] = self.cur.execute('SELECT var{0} FROM Input'.format(i))
dicti['var{0}'.format(i)] = int(self.cur.fetchone()[0])
text = str(dicti['var{0}'.format(i)])
self.ui.textBrowser.append(text)
result = dicti['var1'] + dicti['var2']
textsep = '-'*25+'(+)'
text = str(result)
self.ui.textBrowser.append(text)
self.ui.textBrowser.append(text)
def print_sub(self):
dicti={}
for i in range(2):
i = i + 1
dicti['var{0}'.format(i)] = self.cur.execute('SELECT var{0} FROM Input'.format(i))
dicti['var{0}'.format(i)] = int(self.cur.fetchone()[0])
text = str(dicti['var{0}'.format(i)])
self.ui.textBrowser.append(text)
result = dicti['var1'] + dicti['var2']
textsep = '-'*25+'(-)'
text = str(result)
self.ui.textBrowser.append(text)
self.ui.textBrowser.append(text)
if __name__=='__main__':
app = QtGui.QApplication(sys.argv)
myapp = ShowAndInput()
myapp.show()
sys.exit(app.exec_())
When I ran it, everything was fine. But when I pressed the calculate button, it returned an error message.
Here is the error message: RuntimeError: super-class init() of type Calculate was never called.
Any respond will be much appreciated. Thanks a lot.
Regards,
Arnold
I am designing a program composed of a 3D viewer and a table with Python 3.4 and PySide bindings.
I have created a TableView with this class:
from PySide import QtGui
from PySide.QtCore import Qt
class MyTableView(QtGui.QWidget):
def __init__(self, parent=None):
super(MyTableView, self).__init__()
self.parent = parent
self.title = "Results"
self.initUI()
def initUI(self):
self.grid = QtGui.QGridLayout(self)
self.table = QtGui.QTableView()
self.grid.addWidget(self.table, 0, 0)
# Configure table
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setDefaultAlignment(Qt.AlignLeft)
self.table.setSortingEnabled(True)
self.table.setAlternatingRowColors(True)
self.table.setShowGrid(False)
self.table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
and the model with this other class:
class MyModel(QStandardItemModel):
def __init__(self, path, *args, **kwargs):
super(MyModel, self).__init__()
self.path = path
self.parse()
def parse(self):
with open(self.path) as f:
self.mydata = yaml.load(f)
self.setColumnCount(len(self.mydata['headers']) + 1)
self.setHorizontalHeaderLabels(
['ID'] + self.mydata['headers'])
row = 0
for ind, val in self.mydata['rows'].items():
col = 0
self.insertRow(row)
self.setItem(row, col, QStandardItem(ind))
for v in val:
col += 1
self.setItem(row, col, QStandardItem(str(v)))
row += 1
which are then tied together in this controller:
from PySide.QtCore import Qt
class MyController(object):
def __init__(self, model, view):
self.model = model
self.tableview = view.table
self.fill_table()
self.connect_signals()
def fill_table(self):
self.tableview.setModel(self.model)
self.tableview.sortByColumn(0, Qt.AscendingOrder)
def connect_signals(self):
selectionModel = self.tableview.selectionModel()
selectionModel.selectionChanged.connect(self.selection_changed)
def selection_changed(self, selected, deselected):
print("Selection changed.")
Then, the program is executed through this script:
def main():
app = QtGui.QApplication(sys.argv)
MyController(MyModel(sys.argv[1]), MyView())
sys.exit(app.exec_())
if __name__ == '__main__':
main()
(Note that I haven't posted the main window class, but you get the idea)
The table gets rendered OK, but I am not able to connect the selectionChanged signal to the handler (which should udpate the viewer, but for testing purposes it's only a print statement).
What am I doing wrong? Thanks!
[EDIT]
I have discovered that it works if I use a lambda function to call the handler method. Can someone explain why?!
selectionModel.selectionChanged.connect(lambda: self.selection_changed(selectionModel.selectedRows()))
I tried to implement what you wrote and it worked - so I can't be 100% sure of why you are having trouble. But I suspect it is because of what I had to fix to get it to work at all: I had to sort out some problems you have with garbage collection.
In the example code you give, you create a MyController, a MyModel and a MyView. But they will all then be garbage collected (in CPython) since you don't keep a reference to them. If you add a reference to MyController
my_controller = MyController(MyModel(sys.argv[1]), MyView())
you are almost there, but I think the MyTableView might also then be garbage collected since the controller only keeps a reference to the QTableVIew not the MyTableView.
Presumably using the lanbda function changes the references you are preserving - it preserves the controller and the selection model - and that may be why it is working in that case.
Generally it's a good idea to use the Qt parenting mechanism. If you simply parented all these objects on the main window (or their natural parent widget) that would have prevented most of these problems.