I wanted to test PyQt to write a quick app to display and edit data in an Excel like form but the data is never shown.
Both the docs and the book I read say that using .setItem(row, colum, QTableWidgetItem(data)) on a QtableWidget object is one way to go.
However, the following code doesn't work, I only have an empty table and I can't figure out why. Any idea ?
import sys
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QTableWidget,
QTableWidgetItem,
QMenu,
QAction,
QInputDialog,
)
class SpreadsheetFramework(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setMinimumSize(1000, 500)
self.setWindowTitle("Spreadsheet Table")
# Used for copy and paste actions
self.item_text = None
self.createTable()
self.fillTable()
self.show()
def createTable(self):
self.table_widget = QTableWidget()
self.table_widget.setRowCount(10)
self.table_widget.setColumnCount(10)
self.table_widget.setCurrentCell(0, 0)
self.setCentralWidget(self.table_widget)
def fillTable(self):
for i in range(10):
for j in range(10):
self.table_widget.setItem(i, j, QTableWidgetItem(i * j))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SpreadsheetFramework()
sys.exit(app.exec_())
This is what the window looks like when I run the code
If you want to display the data in a QTableWidgetItem and pass it through the constructor then it must be a string.
self.table_widget.setItem(i, j, QTableWidgetItem(str(i * j)))
The downside is that it is no longer a number but a string that represents a number.
Another better option is to use setData to pass the number to the Qt.DisplayRole role.
item = QTableWidgetItem()
item.setData(Qt.DisplayRole, i * j)
self.table_widget.setItem(i, j, item)
Related
I am trying to code an application that will allow the user to view a list of Tag IDs, as well as its description, and allow the user to check off each Tag ID that they would like to import data from. At this point I am working on developing the UI only.
The code below worked and would show the application window until I added the itemChanged function & connection. Now, when I run this code, only the print statement from the new function will show. The window never shows and the entire application promptly exits (see image for outcome of running script).
Additionally, you'll notice that we get the checkState of each type of item - I only want the checkState of the Tag ID.
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTableView, QHeaderView, QVBoxLayout, QAbstractItemView
from PyQt5.QtCore import Qt, QSortFilterProxyModel
from PyQt5.QtGui import QStandardItemModel, QStandardItem
class myApp(QWidget):
def __init__(self):
super().__init__()
self.resize(1000, 500)
mainLayout = QVBoxLayout()
tagIDs = ('Tag_1', 'Tag_2', 'Tag_3', 'Tag_4', 'Tag_5')
descriptions = ('Description_1', 'Description_2', 'Description_3', 'Description_4', 'Description_5')
model = QStandardItemModel(len(tagIDs), 2)
model.itemChanged.connect(self.itemChanged)
model.setHorizontalHeaderLabels(['Tag IDs', 'Description'])
for i in range(len(tagIDs)):
item1 = QStandardItem(tagIDs[i])
item1.setCheckable(True)
item2 = QStandardItem(descriptions[i])
model.setItem(i, 0, item1)
model.setItem(i, 1, item2)
filterProxyModel = QSortFilterProxyModel()
filterProxyModel.setSourceModel(model)
filterProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
filterProxyModel.setFilterKeyColumn(1)
searchField = QLineEdit()
searchField.setStyleSheet('font-size: 20px; height: 30px')
searchField.textChanged.connect(filterProxyModel.setFilterRegExp)
mainLayout.addWidget(searchField)
table = QTableView()
table.setStyleSheet('font-size: 20px;')
table.verticalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
table.setModel(filterProxyModel)
table.setEditTriggers(QAbstractItemView.NoEditTriggers)
mainLayout.addWidget(table)
self.setLayout(mainLayout)
def itemChanged(self, item):
print("Item {!r} checkState: {}".format(item.text(), item.checkState()))
def main():
app = QApplication(sys.argv)
myAppControl = myApp()
myAppControl.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Header settings that depend on the model must always be set when a model is set.
Move table.setModel(filterProxyModel) right after the creation of the table or, at least, before table.horizontalHeader().setSectionResizeMode (the vertical setSectionResizeMode() is generic for the whole header and doesn't cause problems).
My code is supposed to add data to a two column table when "add" button is clicked. The problem is that when the "add" button is clicked, only the empty row is being added. Can someone please let me know what is wrong? Below is the part of the code that adds data1 and data2 to a table on the right side of the layout. The function add_entry is where the data is being added.
# Import dependencies
from PyQt5.QtWidgets import (QWidget, QApplication, QTableWidget, QTableWidgetItem,QHBoxLayout, QVBoxLayout, QHeaderView, QPushButton, QDialog,
QLabel, QFileDialog, QMainWindow, QAction, QLineEdit)
from PyQt5.Qt import Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtChart import QChart, QChartView, QLineSeries
import sys
import pandas as pd
import math
# ------------------------------------------------------UI-main----------------------------------------------------------------------------------
# Creates a QApplication instance
class MyApp(QWidget):
def __init__(self):
super().__init__()
self.items=0
# Creates table on the left size
self.table_l = QTableWidget()
self.table_l.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
# Creates layout object for the right side
self.layoutRight = QVBoxLayout()
# Creates chart widget
self.chartView = QChartView()
# Smooths the edge of the chart
self.chartView.setRenderHint(QPainter.Antialiasing)
# Creates table on the right size
self.table_r = QTableWidget()
self.table_r.setColumnCount(2)
# self.table_r.setRowCount()
self.table_r.setHorizontalHeaderLabels(('Data1', 'Data2'))
self.table_r.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.table_r.setMaximumSize(600, 300)
self.lineEditData1 = QLineEdit()
self.lineEditData2 = QLineEdit()
# Create push buttons
self.buttonAdd = QPushButton('Add')
self.buttonClear = QPushButton('Clear')
self.buttonQuit = QPushButton('Quit')
self.buttonAdd.setEnabled(False)
self.layoutRight.setSpacing(10)
self.layoutRight.addWidget(self.table_r, 50)
self.layoutRight.addWidget(QLabel('data1'))
self.layoutRight.addWidget(self.lineEditData1)
self.layoutRight.addWidget(QLabel('data2'))
self.layoutRight.addWidget(self.lineEditData2)
self.layoutRight.addWidget(self.buttonAdd)
self.layout = QHBoxLayout()
self.layout.addWidget(self.table_l, 50)
self.setLayout(self.layout)
self.layout.addLayout(self.layoutRight, 50)
# Connect button to function functions
self.buttonQuit.clicked.connect(lambda:app.quit())
self.buttonAdd.clicked.connect(self.add_entry)
self.buttonClear.clicked.connect(self.reset_table)
self.lineEditData1.textChanged[str].connect(self.check_disable)
self.lineEditData2.textChanged[str].connect(self.check_disable)
def add_entry(self):
Data1 = self.lineEditData1.text()
Data2 = self.lineEditData2.text()
try:
Data1Item = QTableWidgetItem(int(Data1))
Data2Item = QTableWidgetItem(float(Data2))
Data2Item.setTextAlignment(Qt.AlignRight | Qt.AlignCenter)
self.table_r.insertRow(self.items)
self.table_r.setItem(self.items, 0, Data1Item)
self.table_r.setItem(self.items, 1, Data2Item)
self.items +=1
# after passing the item, clear the field by entering an empty string
self.lineEditData1.setText('')
self.lineEditData2.setText('')
except ValueError:
pass
# Creates main window object instance
class MainWindow(QMainWindow):
def __init__(self, widget):
super().__init__()
self.setWindowTitle('test')
self.resize(1200, 1200)
self.menuBar = self.menuBar()
self.fileMenu = self.menuBar.addMenu('File')
# import wind speed data
importAction = QAction('Open File', self)
importAction.setShortcut('Ctrl+O')
# exit action
exitAction = QAction('Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.triggered.connect(lambda: app.quit())
self.fileMenu.addAction(importAction)
self.fileMenu.addAction(exitAction)
self.setCentralWidget(widget)
if __name__ =='__main__':
# don't auto scale when drag app to a different monitor
#QGuiApplication.setHightDpiScaleFactorRoundingPolicy(Qt.HightDpiScaleFactorRoundingPolicy.PassThrough)
app = QApplication(sys.argv)
w = MyApp()
demo = MainWindow(w)
demo.show()
try:
sys.exit(app.exec())
except SystemExit:
print('Closing window...')
The objective of exceptions is not to hide errors but to know how to prevent them, so they must be as small as possible so as not to hide other errors. In this case, QTableWidgetItem accepts a string as an argument and not numerical values, therefore an exception is thrown preventing the code that adds the items from being executed. The solution is to use the setData() method of the QTableWidgetItem:
def add_entry(self):
data1 = self.lineEditData1.text()
data2 = self.lineEditData2.text()
try:
value1 = int(data1)
value2 = float(data2)
except ValueError:
print("failed conversion")
return
else:
data1_item = QTableWidgetItem()
data1_item.setData(Qt.DisplayRole, value1)
data2_item = QTableWidgetItem()
data2_item.setData(Qt.DisplayRole, value2)
data2_item.setTextAlignment(Qt.AlignRight | Qt.AlignCenter)
row = self.table_r.rowCount()
self.table_r.insertRow(row)
self.table_r.setItem(row, 0, data1_item)
self.table_r.setItem(row, 1, data2_item)
self.lineEditData1.clear()
self.lineEditData2.clear()
How do i make my table window scale, so i dont have gaps on the side of it. I want it to scale with the size of the contents, and in this case the variable myList can have different amounts of items in it. Doing what i am now, leaves a ~20px gap between the edges of the table, and the edges of the widget
I would like to remove the inner border, so there is only a border between the widget and the window, but not between the table and the widget. But i dont know how.
I want to remove the green area I left some unpainted so you can see it.
Here is the code:
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QMainWindow, QApplication, QTableWidget, QTableWidgetItem, QWidget, QVBoxLayout
import sys
class Main(QMainWindow):
def __init__(self):
super(Main, self).__init__()
self.setupUi()
def setupUi(self):
myList = ["Hello", "World", "!"]
self.table = QTableWidget(self)
self.table.setRowCount(3)
self.table.setColumnCount(len(myList))
for y, x in enumerate(myList):
self.table.setItem(1, y, QTableWidgetItem(x))
for x in range(self.table.columnCount()):
self.table.setItem(2, x, QTableWidgetItem(""))
self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.main_widget = QWidget()
layout = QVBoxLayout(self.main_widget)
layout.addWidget(self.table)
self.main_widget.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
M = Main()
sys.exit(app.exec())
I am using self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) right now, but it doesn't seem to be doing the job.
I am trying to pre-select multiple "individual values (or cells some would like to call em)" from the QTableWidget and I don't seem to be able to find the right method. I have tried setRangeSelect,selectRow & selectColumn, and none of the methods works.
Looking for some help. (Please see the test method for what I am trying to do)
import sys
import json
from PyQt5.QtWidgets import QApplication, QWidget, QTableWidget, QPushButton
from PyQt5.Qt import QTableWidgetItem, QAbstractItemView
class Demo(QWidget):
def __init__(self):
super().__init__()
self.rowTracker = []
self.initUI()
self.initQTableWidget()
def initUI(self):
self.resize(600, 600)
# populate some data
self.rows = [['a1','b1', 'c1'], ['a2','b2','c2'], ['a3','b3','c3'], ['a4','b4','c4'], ['a5','b5','c5']]
self.btn = QPushButton(self)
self.btn.move(50, 250)
self.btn.resize(150, 40)
self.btn.setText('Check')
self.btn.clicked.connect(self.test)
def initQTableWidget(self):
self.tableWidget = QTableWidget(self)
self.tableWidget.resize(self.width(), self.height()-400)
self.tableWidget.setRowCount(len(self.rows))
self.tableWidget.setColumnCount(len(self.rows[0]))
# here we will change row selection behavior to multiselection
self.tableWidget.setSelectionMode(QAbstractItemView.MultiSelection)
for row in enumerate(self.rows):
# print(row)
for col in enumerate(row[1]):
item = QTableWidgetItem()
item.setText(col[1])
self.tableWidget.setItem(row[0], col[0], item)
def test(self):
# print(dir(self.tableWidget))
self.tableWidget.select('<2nd row>', '<1st column>')
self.tableWidget.select('<3nd row>', '<2nd column>')
# self.tableWidget.setRangeSelect()
app =QApplication(sys.argv)
widget = Demo()
widget.show()
sys.exit(app.exec_())
There are the following methods:
The setSelected() method of QtableWidgetItem:
self.tableWidget.item(1, 0).setSelected(True)
self.tableWidget.item(2, 1).setSelected(True)
The select() method of QItemSelectionModel:
model = self.tableWidget.model()
selection_model = self.tableWidget.selectionModel()
selection_model.select(model.index(1, 0), QItemSelectionModel.Select)
selection_model.select(model.index(2, 1), QItemSelectionModel.Select)
The second method is general for all views that inherit from QAbstractItemView, and the first method is just a wrapper that makes QTableWidget of the second method.
I would like to add various widgets to various cells in a Table Widget, and I would like to trigger commands when those widgets' values are changed. I can get the widgets into the table as desired, but I'm having problems connecting signals so that I know which widget has generated the signal.
Below is a simple example explaining the problem, using just checkboxes:
from PyQt5 import QtWidgets, QtGui, QtCore
class Main(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# create table:
self.table = QtWidgets.QTableWidget()
[self.table.insertRow(i) for i in [0,1,2]]
[self.table.insertColumn(i) for i in [0,1]]
# set values for first column:
self.table.setItem(0, 0, QtWidgets.QTableWidgetItem('A') )
self.table.setItem(1, 0, QtWidgets.QTableWidgetItem('B') )
self.table.setItem(2, 0, QtWidgets.QTableWidgetItem('C') )
# add checkboxes to second column:
cb0 = QtWidgets.QCheckBox( parent=self.table )
cb1 = QtWidgets.QCheckBox( parent=self.table )
cb2 = QtWidgets.QCheckBox( parent=self.table )
self.table.setCellWidget(0, 1, cb0)
self.table.setCellWidget(1, 1, cb1)
self.table.setCellWidget(2, 1, cb2)
# connect table signals:
self.table.cellChanged.connect(self.cell_changed)
self.table.itemChanged.connect(self.item_changed)
# connect checkbox signals:
cb0.clicked.connect(self.checkbox_clicked)
cb1.clicked.connect(self.checkbox_clicked)
cb2.clicked.connect(self.checkbox_clicked)
# show:
self.setCentralWidget(self.table)
self.setWindowTitle('TableWidget, CheckBoxes')
self.show()
def cell_changed(self, row, col):
print(row, col)
def checkbox_clicked(self, checked):
print(checked)
def item_changed(self, item):
print(item)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = Main()
app.exec_()
Based on table.cellChanged.connect I would naively expect a cellChanged signal when the checkboxes are changed. However this signal is not generated. Nor is the itemChanged signal. I can indeed see the clicked signals, but that is not very useful because it is unclear which checkbox has produced the signal.
One way to solve the problem is to create a different checkbox_clicked function for each checkbox, but that hardly seems elegant.
My questions are:
Why is neither a cellChanged nor an itemChanged signal generated when a checkbox is changed?
How should signals be connected in order to know which checkbox has generated the clicked signal?
Why is neither a cellChanged nor an itemChanged signal generated when
a checkbox is changed?
because when you use setCellWidget() a QTableWidgetItem is not created, and if we check the documentation of cellChanged and itemChanged:
void QTableWidget::cellChanged(int row, int column)
This signal is emitted whenever the data of the item in the cell specified by row and column has changed.
void QTableWidget::itemChanged(QTableWidgetItem *item)
This signal is emitted whenever the data of item has changed.
How should signals be connected in order to know which checkbox has generated the clicked signal?
The way to obtain is indirectly, the first thing to know is that when the widget is added through the setCellWidget() method, the viewport() of the QTableWidget is set as a parent.
Also another thing that should be known is that the position of a widget that is accessed through pos() is relative to the parent, that is, in our case relative to viewport().
There is a very useful method called sender() that returns the object that emits the signal, in this case it will return the QCheckBox.
As the position of the widget with respect to the viewport() is known, its QModelIndex is accessed through the indexAt() method, the QModelIndex has the information of the cell.
All of the above is implemented in the following example:
from PyQt5 import QtWidgets, QtGui, QtCore
class Main(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# create table:
self.table = QtWidgets.QTableWidget()
self.table.setRowCount(3)
self.table.setColumnCount(2)
for i, letter in enumerate("ABC"):
self.table.setItem(i, 0, QtWidgets.QTableWidgetItem(letter))
for i in range(self.table.rowCount()):
ch = QtWidgets.QCheckBox(parent=self.table)
ch.clicked.connect(self.onStateChanged)
self.table.setCellWidget(i, 1, ch)
self.setCentralWidget(self.table)
self.setWindowTitle('TableWidget, CheckBoxes')
self.show()
def onStateChanged(self):
ch = self.sender()
print(ch.parent())
ix = self.table.indexAt(ch.pos())
print(ix.row(), ix.column(), ch.isChecked())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = Main()
sys.exit(app.exec_())
Another way to do it is through lambda methods or partial.functions where we pass directly new parameters.
from PyQt5 import QtWidgets, QtGui, QtCore
class Main(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# create table:
self.table = QtWidgets.QTableWidget()
self.table.setRowCount(3)
self.table.setColumnCount(2)
for i, letter in enumerate("ABC"):
self.table.setItem(i, 0, QtWidgets.QTableWidgetItem(letter))
for i in range(self.table.rowCount()):
ch = QtWidgets.QCheckBox(parent=self.table)
ch.clicked.connect(lambda checked, row=1, col=i: self.onStateChanged(checked, row, col))
self.table.setCellWidget(i, 1, ch)
self.setCentralWidget(self.table)
self.setWindowTitle('TableWidget, CheckBoxes')
self.show()
def onStateChanged(self, checked, row, column):
print(checked, row, column)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = Main()
sys.exit(app.exec_())
If you want to know more information how to pass extra parameters through connect() you can review this answer.
use the stateChanged signal for checkboxes.
and my take about that code:
in some cases it's helpful to have a reference to checkbox widgets, for some logic actions.
use loops if possible
use explicit imports in PyQt - the class names are unique and it's more readable
for example:
from PyQt5.QtWidgets import QMainWindow, QTableWidgetItem, QCheckBox, QApplication
from typing import Dict
class Main(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# create table:
self.table = QTableWidget()
self.table.insertColumn(0)
self.table.insertColumn(1)
self._items: Dict[QTableWidgetItem, QCheckBox] = {}
for i, tag in enumerate(['A', 'B', 'C']):
self.table.insertRow(i)
item = QTableWidgetItem(tag)
cb = QCheckBox(parent=self.table)
self._items[item] = cb
# set values for first column:
self.table.setItem(i, 0, item)
# add checkboxes to second column:
self.table.setCellWidget(i, 1, cb)
# connect cb signals:
self._items[item].stateChanged.connect(self.checkbox_clicked)
# connect table signals:
self.table.cellChanged.connect(self.cell_changed)
# show:
self.setCentralWidget(self.table)
self.setWindowTitle('TableWidget, CheckBoxes')
self.show()
def cell_changed(self, row, col):
print(row, col)
def checkbox_clicked(self, checked):
print(checked)
def item_changed(self, item):
print(item)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
main = Main()
app.exec_()