PyQT QPushbutton with toggle function in a table view - python-3.x

I am try to create a Tableview using delegate. Specifically adding a pushbutton at each row
I have added a QPushbutton to a table view as shown below. The pushbutton is checkable, but when the status is checked it always shows false. If I don't use openPersistenteditor, then the push button works but I have to double click.
class ButtonDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
self.button = QtWidgets.QPushButton(parent)
self.button.setCheckable(True)
self.button.setStyleSheet("background: white;")
self.button.toggled.connect(self.commit_data)
self.button
return self.button
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def paint(self, painter, option, index):
value = index.data(QtCore.Qt.EditRole)
opt = QtWidgets.QStyleOptionButton()
opt.state = QtWidgets.QStyle.State_Enabled
opt.state = QtWidgets.QStyle.State_On if value else QtWidgets.QStyle.State_Off
QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_PushButton, opt, painter)
def commit_data(self):
if self.button.isChecked():
self.button.setStyleSheet("background: black;")
else:
self.button.setStyleSheet("background: blue;")
class Model(QtCore.QAbstractTableModel):
def __init__(self, table):
super().__init__()
self.table = table
def rowCount(self, parent):
return len(self.table)
def columnCount(self, parent):
return len(self.table[0])
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
return self.table[index.row()][index.column()]
def setData(self, index, value, role):
if type(role) == QtCore.QVariant:
self.table[index.row()][index.column()] = role.value()
return True
class Main(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
table = [[False] for i in range(50)]
self.model = Model(table)
self.tableView = QtWidgets.QTableView()
self.tableView.setModel(self.model)
self.tableView.setItemDelegateForColumn(0, ButtonDelegate(self))
for row in range(len(table)) :
self.tableView.openPersistentEditor(self.model.index(row, 0))

First of all, you're combining two different ways of displaying a button, and you should only use one of them.
The paint implementation only draws a button, there's no interaction available, you should implement that using editorEvent().
The createEditor() instead actually creates a button, just like it normally creates a line edit for standard text fields.
Using the editor (along with the openPersistentEditor) is probably the simpler choice, but it's a good choice only as long as the model is relatively small. If you plan in having hundreds or thousands of items, the paint implementation is certainly better for performance.
Now, assuming you'll go for the easy way (the editor), the problem is that you're constantly overwriting self.button everytime a new editor is requested. The result is that self.button will always refer to the last button created. Remember, whenever a function dynamically creates a new object (without deleting the previously created ones), setting that object as an instance attribute is pointless, as you're creating a reference that will be overwritten the next time the function gets called.
A possible solution is to use a lambda for the signal, and send the button instance as argument for the function:
class ButtonDelegate(QtWidgets.QItemDelegate):
# ...
def createEditor(self, parent, option, index):
button = QtWidgets.QPushButton(parent)
button.setCheckable(True)
button.setStyleSheet("background: white;")
button.toggled.connect(lambda: self.commit_data(button))
return button
def commit_data(self, button):
if button.isChecked():
button.setStyleSheet("background: black;")
else:
button.setStyleSheet("background: blue;")
A suggestion: use line spacings between functions and classes. Your code is so condensed that it's actually very difficult to read it; adding spaces between functions makes it more easy to read and understand where functions start and end, which dramatically improves readability of your code, which is a very important aspect you should not underestimate. Read more about this and other related subjects in the official Style Guide for Python Code.

Related

validating specific column of QTreeWidget using QStyledItemDelegate not working properly

I have a QTreeWidget with 3 columns, only the 3rd column should have a double validator. My issue is that it somewhat works; The QLineEdit does not stop at 100 it just keeps going.
what I am missing; to make it where it doesn't let user input anything higher than 100 while still editing the lineEdit?
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class Delegate(QStyledItemDelegate):
'Changes number of decimal places in gas analysis self.chosen table'
def __init__(self, decimals, parent=None):
super().__init__(parent)
self.nDecimals = decimals
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
editor.setValidator(QDoubleValidator(0,100, 15))
return editor
def setEditorData(self, editor, index):
if index.column() == 2 and index.data() is not None:
editor.setText(str(float(index.data())))
class Widget(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent=None)
self.tree_widget = QTreeWidget()
self.tree_widget.setItemDelegate(Delegate(self.tree_widget))
self.tree_widget.setHeaderLabels(["Value1", "Value2", "Value3"])
self.setCentralWidget(self.tree_widget)
for vals in [("h", "20.0", "40.0"), ("k", "25.0", "50.0")]:
it = QTreeWidgetItem(vals)
it.setFlags(it.flags()| Qt.ItemIsEditable)
self.tree_widget.addTopLevelItem(it)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
The QDoubleValidator only tries to validate the text entered by the user, if it does not meet the requirement then the information will not be saved and that is what is observed in your case. If you want to define what values you want the user to use in your specific case, it is better to use the QDoubleSpinBox as editor:
class Delegate(QStyledItemDelegate):
def __init__(self, decimals, parent=None):
super().__init__(parent)
self.nDecimals = decimals
def createEditor(self, parent, option, index):
editor = QDoubleSpinBox(
parent, minimum=0, maximum=100.0, decimals=self.nDecimals
)
return editor
def setEditorData(self, editor, index):
if index.data() is not None:
try:
value = float(index.data())
except Exception as e:
print("error", e)
else:
editor.setValue(value)
class Widget(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent=None)
self.tree_widget = QTreeWidget()
self.tree_widget.setHeaderLabels(["Value1", "Value2", "Value3"])
self.tree_widget.setItemDelegateForColumn(2, Delegate(15, self.tree_widget))
self.setCentralWidget(self.tree_widget)
for vals in [("h", "20.0", "40.0"), ("k", "25.0", "50.0")]:
it = QTreeWidgetItem(vals)
it.setFlags(it.flags() | Qt.ItemIsEditable)
self.tree_widget.addTopLevelItem(it)
Note: I have modified the column selection since with the initial implementation of the OP the same editor is used in all the columns and that clearly contradicts its same requirement, so instead of using setItemDelegate() I have used setItemDelegateForColumn().

How to get tkinter window to be already transparent when opening?

I want a black window to fade in. When in fullscreen, it perfectly works but I need a specific size and there when its opened, it first appears black before it becomes transparent and the fading starts. Do you have any ideas how to achieve the same smooth effect as for the fullscreen version?
import tkinter as tk
class Fader(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.parent = parent
self.parent.attributes("-alpha",0.0)
#self.parent.attributes("-fullscreen",True)
self.parent.geometry("600x800")
self.configure(bg='black')
self.fade_in()
def fade_in(self):
alpha = self.parent.attributes("-alpha")
if alpha < 1:
alpha += .01
self.parent.attributes("-alpha", alpha)
self.after(100, self.fade_in)
if __name__ == "__main__":
root = tk.Tk()
root.bind("<Escape>",lambda e: root.destroy())
Fader(root).pack(fill="both", expand=True)
root.mainloop()
You can use withdraw() to hide the window and deiconify() to show to window later on and increase the alpha. But it seems to not work unless you update the tasks or wait for the window to be visible.
Method 1:
Was able to fix this by using update_idletasks(), like:
class Fader(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.parent = parent
self.parent.attributes('-alpha',0.0)
self.parent.withdraw() #hiding the window
#self.parent.attributes("-fullscreen",True)
self.parent.update_idletasks()
self.parent.geometry("600x800")
self.configure(bg='black')
self.fade_in()
def fade_in(self):
self.parent.deiconify() #bringing it back
..... #same code
Method 2:
Or like said by acw1668, you can use wait_visibility(), like:
class Fader(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.parent = parent
self.parent.wait_visibility(self.parent)
self.parent.attributes('-alpha',0.0)
self.parent.withdraw()
# self.parent.attributes("-fullscreen",True)
self.parent.geometry("600x800")
self.configure(bg='black')
self.fade_in()
def fade_in(self):
self.parent.deiconify()
...... #same code
A bit more about wait_visibility():
wait_visibility(window=None)
Wait for the given widget to become visible. This is typically used to wait until a new toplevel window appears on the screen. Like wait_variable, this method enters a local event loop, so other parts of the application will still work as usual.
A bit more about update_idletasks():
update_idletasks()
Calls all pending idle tasks, without processing any other events. This can be used to carry out geometry management and redraw widgets if necessary, without calling any callbacks.
Source :- https://effbot.org/tkinterbook/widget.htm

Using QComboBox in QTableView properly - issues with data being set and clearing QComboBoxes

In my application im using a QTableView, QStandardItemModel and a QSortFilterProxyModel in between for filtering.
The content is updated via a method for columns 1 & 2, and I want there to be a 3rd column for user to select options. I would prefer to use a QComboBox.
I've got everything pretty much working, except that when I select the item from the QComboBox in any of the cells in column 3, it doesn't populate. Does it have something to do with my setModelData() method?
I also have a clear button that I would like to reset all of the QComboBoxes to the first item which is an empty entry. I am not sure how to tackle this, i've found such things as using deleteLater() or setting the QTableView's setItemDelegateForColumn() to None and re-apply.
Obviously these are not the most efficient. What am I missing?
Working example:
import win32com.client
from PyQt5 import QtCore, QtGui, QtWidgets
outApp = win32com.client.gencache.EnsureDispatch("Outlook.Application")
outGAL = outApp.Session.GetGlobalAddressList()
entries = outGAL.AddressEntries
class ComboDelegate(QtWidgets.QItemDelegate):
def __init__(self,parent=None):
super().__init__(parent)
self.items = ['','To', 'CC']
def createEditor(self, widget, option, index):
editor = QtWidgets.QComboBox(widget)
editor.addItems(self.items)
return editor
def setEditorData(self, editor, index):
if index.column() == 2:
editor.blockSignals(True)
text = index.model().data(index, QtCore.Qt.EditRole)
try:
i = self.items.index(text)
except ValueError:
i = 0
editor.setCurrentIndex(i)
editor.blockSignals(False)
else:
QtWidgets.QItemDelegate.setModelData(editor,model,index)
def setModelData(self, editor, model, index):
if index.column() == 2:
model.setData(index, editor.currentText())
else:
QtWidgets.QItemDelegate.setModelData(editor,model,index)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def paint(self, painter, option, index):
QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
class App(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
"""This method creates our GUI"""
self.centralwidget = QtWidgets.QWidget()
self.setCentralWidget(self.centralwidget)
self.lay = QtWidgets.QVBoxLayout(self.centralwidget)
self.filterEdit = QtWidgets.QLineEdit()
self.filterEdit.setPlaceholderText("Type to filter name.")
self.label = QtWidgets.QLabel("Select an option for each person:")
self.button = QtWidgets.QPushButton("Test Button")
self.button.clicked.connect(self.runButton)
self.resetbutton = QtWidgets.QPushButton("Clear")
self.resetbutton.clicked.connect(self.clear)
self.lay.addWidget(self.filterEdit)
self.lay.addWidget(self.label)
self.tableview=QtWidgets.QTableView(self.centralwidget)
self.model=QtGui.QStandardItemModel()
self.model.setHorizontalHeaderLabels(['Name','Address','Option'])
self.tableview.verticalHeader().hide()
self.tableview.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
self.tableview.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers)
self.proxyModel = QtCore.QSortFilterProxyModel(self)
self.proxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.proxyModel.setSourceModel(self.model)
self.proxyModel.sort(0,QtCore.Qt.AscendingOrder)
self.proxyModel.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.tableview.setModel(self.proxyModel)
self.model.insertRow(self.model.rowCount(QtCore.QModelIndex()))
#self.fillModel(self.model) #uncomment if you have outlook
self.tableview.resizeColumnsToContents()
self.tableview.verticalHeader().setDefaultSectionSize(10)
self.filterEdit.textChanged.connect(self.onTextChanged)
self.lay.addWidget(self.tableview)
self.delegate = ComboDelegate()
self.tableview.setItemDelegateForColumn(2, self.delegate)
self.lay.addWidget(self.button)
self.lay.addWidget(self.resetbutton)
self.setMinimumSize(450, 200)
self.setMaximumSize(1500, 200)
self.setWindowTitle('Application')
def clear(self):
###clear tableview comboboxes in column 3
print("clear")
def runButton(self,index):
print("Do stuff")
def fillModel(self,model):
"""Fills model from outlook address book """
nameList = []
addressList = []
for row,entry in enumerate(entries):
if entry.Type == "EX":
user = entry.GetExchangeUser()
if user is not None:
if len(user.FirstName) > 0 and len(user.LastName) > 0:
nameItem = QtGui.QStandardItem(str(user.Name))
emailItem = QtGui.QStandardItem(str(user.PrimarySmtpAddress))
nameItem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
emailItem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
model.appendRow([nameItem,emailItem])
#QtCore.pyqtSlot(str)
def onTextChanged(self, text):
self.proxyModel.setFilterRegExp(text)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = App()
w.show()
sys.exit(app.exec_())
The problem is that you override the paint method unnecessarily since you don't want to customize anything. Before override I recommend you understand what it does and for this you can use the docs or the source code. But to summarize, in the case of the QItemDelegate the paint method establishes the information of the roles in the "option" and then just paints, and within that information is the text. But in your case it is not necessary so there is no need to override. On the other hand, if your delegate has the sole function of establishing a QComboBox then you don't have to verify the columns. Considering all of the above, I have simplified your delegate to:
class ComboDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
self.items = ["", "To", "CC"]
def createEditor(self, widget, option, index):
editor = QtWidgets.QComboBox(widget)
editor.addItems(self.items)
return editor
def setEditorData(self, editor, index):
editor.blockSignals(True)
text = index.model().data(index, QtCore.Qt.EditRole)
try:
i = self.items.index(text)
except ValueError:
i = 0
editor.setCurrentIndex(i)
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText())
On the other hand, the QItemEditorFactory uses the qproperty user as the parameter for the update, and in the case of the QComboBox it is the "currentText", so it can be further simplified using that information:
class ComboDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
self.items = ["", "To", "CC"]
def createEditor(self, widget, option, index):
editor = QtWidgets.QComboBox(widget)
editor.addItems(self.items)
return editor
For the clear method is simple: Iterate over all the rows of the third column and set the empty text:
def clear(self):
# clear tableview comboboxes in column 3
for i in range(self.model.rowCount()):
index = self.model.index(i, 2)
self.model.setData(index, "")

Modify a variable inside another class and file

(Non-English native)
This is tricky to explain. I have 2 windows, each one with their own class created by PyQt5, on 2 different .py files. I want to open the 2nd window from a button inside the first one, and then I want that 2nd window to destroy itself when closed. However, in order to do this, I believe I have to set a specific variable in the 1st window to None, but since that is instanced I can't find out how:
First window in myfile.py
class MainForm(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.window_nuevocliente = None
self.actionNuevo_Cliente.triggered.connect(self.open_secondwindow)
def open_secondwindow(self):
if self.window_nuevocliente is None:
self.window_nuevocliente = addnewclient_logic.AddNewClientForm(self)
self.window_nuevocliente.setAttribute(Qt.WA_DeleteOnClose, True)
self.window_nuevocliente.show()
myapp = MainForm()
Second window in addnewclient_logic.py
import myfile
class AddNewClientForm(QtWidgets.QMainWindow, Ui_AddNewClient):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
def closeEvent(self, event):
# Do closing stuff
event.accept()
# Set the first class var window_nuevocliente back to None
myfile.myapp.window_nuevocliente = None
And here is where I'm stuck:
That last line doesn't work. When I close the window, the DeleteOnClose will totally destroy the window, but the first window will still have it assigned on the window_nuevocliente var and so it fails to re-create it from scratch. If I instead omit the check if it's None, the window can be opened multiple times at the same time (and I don't want that).
Managed to fix it by adding the destroyed signal.
def open_secondwindow(self):
if self.window_nuevocliente is None:
self.window_nuevocliente = addnewclient_logic.AddNewClientForm(self)
self.window_nuevocliente.setAttribute(Qt.WA_DeleteOnClose, True)
self.window_nuevocliente.destroyed.connect(self.reset_nuevocliente)
self.window_nuevocliente.show()
def reset_nuevocliente(self):
self.window_nuevocliente = None
I will accept a better solution, though :P
You can eliminate the window_nuevocliente attribute like this:
addnewclient_logic.py:
class AddNewClientForm(QtWidgets.QMainWindow, Ui_AddNewClient):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
self.setupUi(self)
myfile.py:
from addnewclient_logic import AddNewClientForm
class MainForm(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.actionNuevo_Cliente.triggered.connect(self.open_secondwindow)
def open_secondwindow(self):
window_nuevocliente = self.findChild(AddNewClientForm)
if window_nuevocliente is None:
window_nuevocliente = AddNewClientForm(self)
window_nuevocliente.show()
The parent window will hold a reference to the child window until it deletes itself on close - at which point, it will also be removed from the parent's list of children.

Drag a file into a gui using PySide

Hi I want to drag a file (image) into my gui with PySide, however I can't get it to work. I Cannot get it to go into the dropEvent Function. My object that I am trying to drag into is a QGraphicsView so the filter can't take over the whole gui because I want to drag two images into it.
class Consumer(QMainWindow, Ui_MainWindow, QComboBox, QtGui.QWidget):
def __init__(self, parent=None):
self.paylod = None
super(Consumer, self).__init__(parent)
self.setupUi(self)
self.chkApplyCompression.stateChanged.connect(self.makecompress)
self.viewCarrier1.setMouseTracking(True)
self.viewCarrier1.installEventFilter(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.viewCarrier1)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.QDropEvent and
print('yay?')
return QtGui.QWidget.eventFilter(self, source, event)
def dropEvent(self, e):
print("yay")
def dragEnterEvent(self, *args, **kwargs):
print("Yay!!")
if __name__ == "__main__":
currentApp = QtGui.QApplication(sys.argv)
currentForm = Consumer()
currentForm.show()
currentApp.exec_()
Thanks
You need to accept the drag enter event before Qt will handle a subsequent drop event:
def dragEnterEvent(self, event):
event.accept()

Resources