Im looking to update the temp and humidity in this script.
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QWidget, QApplication, QSplitter, QLabel, QVBoxLayout, QColor
import Adafruit_DHT
import urllib2
from BeautifulSoup import BeautifulSoup
sensor_args = { '11': Adafruit_DHT.DHT11,
'22': Adafruit_DHT.DHT22,
'2302': Adafruit_DHT.AM2302 }
humidity, temperature = Adafruit_DHT.read_retry(11, 4)
temp = 'Temp={0:0.1f}* Humidity={1:0.1f}%'.format(temperature, humidity)
soup = BeautifulSoup(urllib2.urlopen('http://partner.mlb.com/partnerxml/gen/news/rss/bos.xml').read())
title = soup.find('item').title
desc = soup.find('item').description
url = soup.find('item').guid
temperature = temperature * 9/5.0 + 32
class MyWidget(QWidget):
def __init__( self, parent = None ):
super(MyWidget, self).__init__(parent)
# create widgets
a = QLabel('Humidity:{:0.1f}%'.format(humidity),self )
a.setMinimumSize(100, 100)
b = QLabel('Temperature:{:0.1f}F'.format(temperature),self )
b.setMinimumSize(100, 100)
c = QLabel("Redsox News \nTitle: %s\nSummary: %s " % (title.text, desc.text), self)
c.setWordWrap(True)
c.setMinimumSize(280, 200)
d = QLabel("This is some bullshit wordwrap and i cant get it tow work", self)
d.setWordWrap(True)
d.setMinimumSize(180, 300)
for lbl in (a, b, c, d):
lbl.setAlignment(Qt.AlignLeft)
# create 2 horizontal splitters
h_splitter1 = QSplitter(Qt.Horizontal, self)
h_splitter1.addWidget(a)
h_splitter1.addWidget(b)
h_splitter2 = QSplitter(Qt.Horizontal, self)
h_splitter2.addWidget(c)
h_splitter2.addWidget(d)
h_splitter1.splitterMoved.connect(self.moveSplitter)
h_splitter2.splitterMoved.connect(self.moveSplitter)
self._spltA = h_splitter1
self._spltB = h_splitter2
# create a vertical splitter
v_splitter = QSplitter(Qt.Vertical, self)
v_splitter.addWidget(h_splitter1)
v_splitter.addWidget(h_splitter2)
layout = QVBoxLayout()
layout.addWidget(v_splitter)
self.setLayout(layout)
#color widget code
palette = self.palette()
role = self.backgroundRole()
palette.setColor(role, QColor('black'))
self.setPalette(palette)
a.setStyleSheet("QLabel {color:yellow}")
b.setStyleSheet("QLabel {color:yellow}")
c.setStyleSheet("QLabel {background-color: black; color:white}")
d.setStyleSheet("QLabel {background-color: black; color:white}")
#self.setWindowFlags(Qt.CustomizeWindowHint)
timer=self.QTimer()
timer.start(5000)
timer.timeout.connect(self.temp.update)
def moveSplitter( self, index, pos ):
splt = self._spltA if self.sender() == self._spltB else self._spltB
splt.blockSignals(True)
#splt.moveSplitter(index, pos)
splt.blockSignals(False)
if ( __name__ == '__main__' ):
app = QApplication([])
widget = MyWidget()
widget.show()
app.exec_()
Ive been learning a lot about pyQt and all the ins and outs of it. Slow going i might add as i am very new to python.
What I would like is to have it so that this updates the temp and humidity every 5 minutes. I have tried this..
timer=self.QTimer()
timer.start(300)
timer.timeout.connect(self.temp.update)
This does not seem to work for me. I get the error no attribute QTimer.
(Note, I'm not really familiar with pyqt, so if this is wrong, please let me know and I'll delete the answer...)
The line
timer=self.QTimer()
is wrong. this is a QWidget subclass, which does not have QTimer attribute. In fact, QTimer is a regular Qt class, so that line should simply be:
timer = QTimer()
You also need the right import, of course, which I think is:
from PyQt4.QtCore import QTimer
Related
Starting the program, the QIcon is aligned on the left (it's standard i guess) with the text right to it.
Instead I want the icon to be centered on top with the text below it.
I tried using setStyleSheet with show_all.setStyleSheet("QIcon { vertical-align: top }") and show_all.setStyleSheet("QPushButton { text-align: bottom }").
How can I achieve this?
QPushButton doesn't allow to choose the layout of its icon and label. Also, remember that while Qt features style sheets to style widgets, not all CSS known properties and selectors are available. Furthermore, style sheets only work on widgets, so using the QIcon selector isn't supported, since QIcon is not a QWidget subclass.
The most simple solution is to use a QToolButton and set the toolButtonStyle correctly:
self.someButton = QtWidgets.QToolButton()
# ...
self.someButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
The alternative is to subclass the button, provide a customized paint method and reimplement both sizeHint() and paintEvent(); the first is to ensure that the button is able to resize itself whenever required, while the second is to paint the button control (without text!) and then paint both the icon and the text.
Here's a possible implementation:
from PyQt5 import QtCore, QtGui, QtWidgets
class CustomButton(QtWidgets.QPushButton):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._icon = self.icon()
if not self._icon.isNull():
super().setIcon(QtGui.QIcon())
def sizeHint(self):
hint = super().sizeHint()
if not self.text() or self._icon.isNull():
return hint
style = self.style()
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
margin = style.pixelMetric(style.PM_ButtonMargin, opt, self)
spacing = style.pixelMetric(style.PM_LayoutVerticalSpacing, opt, self)
# get the possible rect required for the current label
labelRect = self.fontMetrics().boundingRect(
0, 0, 5000, 5000, QtCore.Qt.TextShowMnemonic, self.text())
iconHeight = self.iconSize().height()
height = iconHeight + spacing + labelRect.height() + margin * 2
if height > hint.height():
hint.setHeight(height)
return hint
def setIcon(self, icon):
# setting an icon might change the horizontal hint, so we need to use a
# "local" reference for the actual icon and go on by letting Qt to *think*
# that it doesn't have an icon;
if icon == self._icon:
return
self._icon = icon
self.updateGeometry()
def paintEvent(self, event):
if self._icon.isNull() or not self.text():
super().paintEvent(event)
return
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
opt.text = ''
qp = QtWidgets.QStylePainter(self)
# draw the button without any text or icon
qp.drawControl(QtWidgets.QStyle.CE_PushButton, opt)
rect = self.rect()
style = self.style()
margin = style.pixelMetric(style.PM_ButtonMargin, opt, self)
iconSize = self.iconSize()
iconRect = QtCore.QRect((rect.width() - iconSize.width()) / 2, margin,
iconSize.width(), iconSize.height())
if self.underMouse():
state = QtGui.QIcon.Active
elif self.isEnabled():
state = QtGui.QIcon.Normal
else:
state = QtGui.QIcon.Disabled
qp.drawPixmap(iconRect, self._icon.pixmap(iconSize, state))
spacing = style.pixelMetric(style.PM_LayoutVerticalSpacing, opt, self)
labelRect = QtCore.QRect(rect)
labelRect.setTop(iconRect.bottom() + spacing)
qp.drawText(labelRect,
QtCore.Qt.TextShowMnemonic|QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop,
self.text())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = CustomButton('Alles anzeigen', icon=QtGui.QIcon.fromTheme('document-new'))
w.setIconSize(QtCore.QSize(32, 32))
w.show()
sys.exit(app.exec_())
Alternatively, try it:
import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (QApplication, QWidget, QGridLayout,
QToolBar, QAction)
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
add_action = QAction(QIcon("img/add.png"), "Add", self)
add_action.triggered.connect(self.addValue)
sub_action = QAction(QIcon("img/min.png"), "Sub", self)
sub_action.triggered.connect(self.subValue)
toolbar = QToolBar()
toolbar.setContentsMargins(0, 0, 0, 0)
toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon | Qt.AlignLeading)
toolbar.setIconSize(QSize(50, 50))
toolbar.addAction(add_action)
toolbar.addAction(sub_action)
rootGrid = QGridLayout(self)
rootGrid.addWidget(toolbar)
def addValue(self):
print("def addValue:")
def subValue(self):
print("def subValue:")
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Widget()
main.show()
sys.exit(app.exec_())
I am writing a small program and ideally I want to have a fixed size of groups which can unfold in which I have further spoilers which represent items which i can open and close in order to add some entities to my system.
I have been looking for similar questions here and got to the following to work semiproperly:
I have added the -Buttons to remove those childs from the groups and a + to add childs to a group.
This seems to work fine as long as I am not removing or adding widgets.
My code looks like this:
spoilers.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Spoiler(QWidget):
def __init__(self, parent=None, title='', animationDuration=300, addRemoveOption='None'):
super(Spoiler, self).__init__(parent=parent)
self.animationDuration = animationDuration
self.toggleAnimation = QParallelAnimationGroup()
self.contentArea = QScrollArea()
self.headerLine = QFrame()
self.toggleButton = QToolButton()
self.mainLayout = QGridLayout()
self.childWidgets = []
self.toggleButton.setStyleSheet("QToolButton { border: none; }")
self.toggleButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggleButton.setArrowType(Qt.RightArrow)
self.toggleButton.setText(str(title))
self.toggleButton.setCheckable(True)
self.toggleButton.setChecked(False)
self.addRemoveOperation = addRemoveOption
if addRemoveOption is not 'None':
if addRemoveOption is 'Add':
self.addRemoveButton = QPushButton('+')
else:
self.addRemoveButton = QPushButton('-')
self.addRemoveButton.clicked.connect(self.onAddRemoveButton)
self.contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }")
self.contentArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
# start out collapsed
self.contentArea.setMaximumHeight(0)
self.contentArea.setMinimumHeight(0)
# let the entire widget grow and shrink with its content
self.toggleAnimation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggleAnimation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
self.toggleAnimation.addAnimation(QPropertyAnimation(self.contentArea, b"maximumHeight"))
# don't waste space
self.mainLayout.setVerticalSpacing(0)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.addWidget(self.toggleButton, 0, 0, 1, 1, Qt.AlignLeft)
if addRemoveOption is not 'None':
self.mainLayout.addWidget(self.addRemoveButton, 0, 2, 1, 1, Qt.AlignRight)
self.mainLayout.addWidget(self.contentArea, 1, 0, 1, 3)
self.setLayout(self.mainLayout)
def start_animation(checked):
arrow_type = Qt.DownArrow if checked else Qt.RightArrow
direction = QAbstractAnimation.Forward if checked else QAbstractAnimation.Backward
self.toggleButton.setArrowType(arrow_type)
self.toggleAnimation.setDirection(direction)
self.toggleAnimation.start()
self.toggleButton.clicked.connect(start_animation)
self.contentLayout = QVBoxLayout()
self.setContentLayout(self.contentLayout)
def setContentLayout(self, contentLayout):
self.contentArea.destroy()
self.contentArea.setLayout(contentLayout)
collapsedHeight = self.sizeHint().height() - self.contentArea.maximumHeight()
contentHeight = contentLayout.sizeHint().height()
for i in range(self.toggleAnimation.animationCount()-1):
spoilerAnimation = self.toggleAnimation.animationAt(i)
spoilerAnimation.setDuration(self.animationDuration)
spoilerAnimation.setStartValue(collapsedHeight)
spoilerAnimation.setEndValue(collapsedHeight + contentHeight)
contentAnimation = self.toggleAnimation.animationAt(self.toggleAnimation.animationCount() - 1)
contentAnimation.setDuration(self.animationDuration)
contentAnimation.setStartValue(0)
contentAnimation.setEndValue(contentHeight)
def addChild(self, child):
self.childWidgets += [child]
self.contentLayout.addWidget(child)
self.setContentLayout(self.contentLayout)
def removeChild(self, child):
self.childWidgets.remove(child)
#self.contentLayout.removeWidget(child)
child.destroy()
#self.setContentLayout(self.contentLayout)
def onAddRemoveButton(self):
self.addChild(LeafSpoiler(root=self))
class LeafSpoiler(Spoiler):
def __init__(self, parent=None, root=None, title=''):
if(root == None):
addRemoveOption = 'None'
else:
addRemoveOption = 'Sub'
super(LeafSpoiler, self).__init__(parent=parent, title=title, addRemoveOption=addRemoveOption)
self.root = root
def onAddRemoveButton(self):
self.root.removeChild(self)
gui.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from spoilers import *
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setGeometry(300, 300, 300, 220)
# self.setWindowIcon(QIcon('web.png'))
self.centralWidget = QFrame()
self.centralLayout = QVBoxLayout()
self.centralWidget.setLayout(self.centralLayout)
self.spoiler1 = Spoiler(addRemoveOption='Add', title='Group 1')
self.spoiler2 = Spoiler(addRemoveOption='Add', title='Group 2')
for i in range(3):
leaf = LeafSpoiler(root=self.spoiler1)
self.spoiler1.addChild(leaf)
leaf = LeafSpoiler(root=self.spoiler2)
self.spoiler2.addChild(leaf)
self.centralLayout.addWidget(self.spoiler1)
self.centralLayout.addWidget(self.spoiler2)
self.setCentralWidget(self.centralWidget)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
I am not quiet sure why this doesnt work. I assume that Spoiler.setContentLayout() is not supposed to be called more than once.
I would be very happy if someone could help me out on this one!
Greetings,
Finn
I am not too sure whether I understood your question correctly. I assume you are talking about pyqt crashing when trying to remove a Spoilerleaf? At least this is what's happening on my machine.
Your "removeChild" method seems to be a culprit here. Without knowing too much about the source of the crash, replacing this with a call to deleteLater() enables child deletion on my machine:
class LeafSpoiler(Spoiler):
# [...] same init as your's
def onAddRemoveButton(self):
self.deleteLater()
Hello Experts!! I hope you are having great day. I am new in GUI programming specially PyQt5. I am practicing on simple GUI invoice application. In this application, I successfully generated the Invoice By QTextDocument. Now i want to add print dialogue and print preview option. I am having trouble in the code. This is saying
AttributeError: 'InvoiceForm' object has no attribute
'printpreviewDialog
As i am new, i am little bit confused in there. Could you please fix the code? That will help me a lot to study. Many Many Thanks.
The code has given below:-
import sys
from PyQt5.QtCore import pyqtSignal, QSize, QSizeF, QDate
from PyQt5.QtGui import QTextDocument, QTextCursor, QFont
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtWidgets import QWidget, QFormLayout, QLineEdit, QPlainTextEdit, QSpinBox, QDateEdit, QTableWidget, \
QHeaderView, QPushButton, QHBoxLayout, QTextEdit, QApplication, QMainWindow
font= QFont('Arial',16)
class InvoiceForm(QWidget):
submitted = pyqtSignal(dict)
def __init__(self):
super().__init__()
self.setLayout(QFormLayout())
self.inputs = dict()
self.inputs['Customer Name'] = QLineEdit()
self.inputs['Customer Address'] = QPlainTextEdit()
self.inputs['Invoice Date'] = QDateEdit(date=QDate.currentDate(), calendarPopup=True)
self.inputs['Days until Due'] = QSpinBox()
for label, widget in self.inputs.items():
self.layout().addRow(label, widget)
self.line_items = QTableWidget(rowCount=10, columnCount=3)
self.line_items.setHorizontalHeaderLabels(['Job', 'Rate', 'Hours'])
self.line_items.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.layout().addRow(self.line_items)
for row in range(self.line_items.rowCount()):
for col in range(self.line_items.columnCount()):
if col > 0:
w = QSpinBox()
self.line_items.setCellWidget(row, col, w)
submit = QPushButton('Create Invoice', clicked=self.on_submit)
print = QPushButton('Print Invoice', clicked=self.printpreviewDialog)
self.layout().addRow(submit,print)
def on_submit(self):
data = {'c_name': self.inputs['Customer Name'].text(),
'c_addr': self.inputs['Customer Address'].toPlainText(),
'i_date': self.inputs['Invoice Date'].date().toString(),
'i_due': self.inputs['Invoice Date'].date().addDays(self.inputs['Days until Due'].value()).toString(),
'i_terms': '{} days'.format(self.inputs['Days until Due'].value()),
'line_items': list()}
for row in range(self.line_items.rowCount()):
if not self.line_items.item(row, 0):
continue
job = self.line_items.item(row, 0).text()
rate = self.line_items.cellWidget(row, 1).value()
hours = self.line_items.cellWidget(row, 2).value()
total = rate * hours
row_data = [job, rate, hours, total]
if any(row_data):
data['line_items'].append(row_data)
data['total_due'] = sum(x[3] for x in data['line_items'])
self.submitted.emit(data)
# remove everything else in this function below this point
class InvoiceView(QTextEdit):
dpi = 72
doc_width = 8.5 * dpi
doc_height = 6 * dpi
def __init__(self):
super().__init__(readOnly=True)
self.setFixedSize(QSize(self.doc_width, self.doc_height))
def build_invoice(self, data):
document = QTextDocument()
self.setDocument(document)
document.setPageSize(QSizeF(self.doc_width, self.doc_height))
document.setDefaultFont(font)
cursor = QTextCursor(document)
cursor.insertText(f"Customer Name: {data['c_name']}\n")
cursor.insertText(f"Customer Address: {data['c_addr']}\n")
cursor.insertText(f"Date: {data['i_date']}\n")
cursor.insertText(f"Total Due: {data['total_due']}\n")
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
central = QWidget()
self.setCentralWidget(central)
layout = QHBoxLayout(central)
self.invoiceForm = InvoiceForm()
layout.addWidget(self.invoiceForm)
self.invoiceView = InvoiceView()
layout.addWidget(self.invoiceView)
# hide the widget right now...
self.invoiceView.setVisible(False)
self.invoiceForm.submitted.connect(self.showPreview)
def showPreview(self, data):
self.invoiceView.setVisible(True)
self.invoiceView.build_invoice(data)
def printpreviewDialog(self):
printer = QPrinter(QPrinter.HighResolution)
previewDialog = QPrintPreviewDialog(printer, self)
previewDialog.paintRequested.connect(self.printPreview)
previewDialog.exec_()
def printPreview(self, printer):
self.invoiceView.build_invoice.print_(printer)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
if __name__ == '__main__':
main()
The main problem is that self.printpreviewDialog is a member of MainWindow, not of InvoiceForm, so you should connect the clicked signal from the main window instead.
Also note that you tried to use self.invoiceView.build_invoice.print_(), but this wouldn't work as you are not calling build_invoice, and even if you did, that function doesn't return anything.
You should use the self.invoiceView.document() instead, but you must ensure that the data has been built before.
class InvoiceForm(QWidget):
submitted = pyqtSignal(dict)
def __init__(self):
# ...
submit = QPushButton('Create Invoice', clicked=self.on_submit)
# make the button a member of the instance instead of a local variable,
# so that we can connect from the main window instance
self.printButton = QPushButton('Print Invoice')
self.layout().addRow(submit, self.printButton)
# ...
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.invoiceForm.printButton.clicked.connect(self.printpreviewDialog)
# ...
def printPreview(self, printer):
self.invoiceView.document().print_(printer)
Note: never, never use built-in functions and statements for variable names, like print.
Try it:
import sys
from PyQt5.QtCore import pyqtSignal, QSize, QSizeF, QDate
from PyQt5.QtGui import QTextDocument, QTextCursor, QFont
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtWidgets import (QWidget, QFormLayout, QLineEdit, QPlainTextEdit,
QSpinBox, QDateEdit, QTableWidget, QHeaderView, QPushButton, QHBoxLayout,
QTextEdit, QApplication, QMainWindow)
font = QFont('Arial',16)
class InvoiceForm(QWidget):
submitted = pyqtSignal(dict)
def __init__(self, parent=None): # + parent=None
super().__init__(parent) # + parent
self.setLayout(QFormLayout())
self.inputs = dict()
self.inputs['Customer Name'] = QLineEdit()
self.inputs['Customer Address'] = QPlainTextEdit()
self.inputs['Invoice Date'] = QDateEdit(date=QDate.currentDate(), calendarPopup=True)
self.inputs['Days until Due'] = QSpinBox()
for label, widget in self.inputs.items():
self.layout().addRow(label, widget)
self.line_items = QTableWidget(rowCount=10, columnCount=3)
self.line_items.setHorizontalHeaderLabels(['Job', 'Rate', 'Hours'])
self.line_items.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.layout().addRow(self.line_items)
for row in range(self.line_items.rowCount()):
for col in range(self.line_items.columnCount()):
if col > 0:
w = QSpinBox()
self.line_items.setCellWidget(row, col, w)
submit = QPushButton('Create Invoice', clicked=self.on_submit)
# + vvvvvv vvvvvvvvvvvvv
_print = QPushButton('Print Invoice', clicked=self.window().printpreviewDialog) # + _print, + self.window()
self.layout().addRow(submit, _print) # + _print
def on_submit(self):
data = {'c_name': self.inputs['Customer Name'].text(),
'c_addr': self.inputs['Customer Address'].toPlainText(),
'i_date': self.inputs['Invoice Date'].date().toString(),
'i_due': self.inputs['Invoice Date'].date().addDays(self.inputs['Days until Due'].value()).toString(),
'i_terms': '{} days'.format(self.inputs['Days until Due'].value()),
'line_items': list()}
for row in range(self.line_items.rowCount()):
if not self.line_items.item(row, 0):
continue
job = self.line_items.item(row, 0).text()
rate = self.line_items.cellWidget(row, 1).value()
hours = self.line_items.cellWidget(row, 2).value()
total = rate * hours
row_data = [job, rate, hours, total]
if any(row_data):
data['line_items'].append(row_data)
data['total_due'] = sum(x[3] for x in data['line_items'])
self.submitted.emit(data)
# remove everything else in this function below this point
# +
return data # +++
class InvoiceView(QTextEdit):
dpi = 72
doc_width = 8.5 * dpi
doc_height = 6 * dpi
def __init__(self):
super().__init__(readOnly=True)
self.setFixedSize(QSize(self.doc_width, self.doc_height))
def build_invoice(self, data):
document = QTextDocument()
self.setDocument(document)
document.setPageSize(QSizeF(self.doc_width, self.doc_height))
document.setDefaultFont(font)
cursor = QTextCursor(document)
cursor.insertText(f"Customer Name: {data['c_name']}\n")
cursor.insertText(f"Customer Address: {data['c_addr']}\n")
cursor.insertText(f"Date: {data['i_date']}\n")
cursor.insertText(f"Total Due: {data['total_due']}\n")
# +
return document # +++
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
central = QWidget()
self.setCentralWidget(central)
layout = QHBoxLayout(central)
# + vvvv
self.invoiceForm = InvoiceForm(self) # + self
layout.addWidget(self.invoiceForm)
self.invoiceView = InvoiceView()
layout.addWidget(self.invoiceView)
# hide the widget right now...
self.invoiceView.setVisible(False)
self.invoiceForm.submitted.connect(self.showPreview)
def showPreview(self, data):
self.invoiceView.setVisible(True)
self.invoiceView.build_invoice(data)
# +++ vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
def printpreviewDialog(self):
previewDialog = QPrintPreviewDialog()
previewDialog.paintRequested.connect(self.printPreview)
previewDialog.exec_()
def printPreview(self, printer):
# self.invoiceView.build_invoice.print_(printer)
data = self.invoiceForm.on_submit()
document = self.invoiceView.build_invoice(data)
document.print_(printer)
# +++ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
if __name__ == '__main__':
main()
I am coding a GUI program now and I have two separate PyQt5 widget objects that need to communicate with each other. I have something that works now (which I have provided a simplified example of below), but I suspect there is a more robust way of doing this that I am hoping to learn about. I will summarize the functionality below, for those that would like a bit of an intro to the code.
TL;DR: Please help me find a better way to use a button click in object 1 to change a variable in object 2 that sends the coordinates of a mouse click in object 2 to object 1 where those coordinates populate two spin boxes.
This first MainWindow class is where the widget objects are defined. The two objects of interest are MainWindow.plotWidget, an instance of the MplFig class, and MainWindow.linePt1, an instance of the LineEndpoint class. Note here that I am able to pass the self.plotWidget as an argument into the LineEndpoint object, but since MainWindow.plotWidget is defined first, I cannot pass self.linePt1 as an argument there.
The functionality I have achieved with these widgets is a button in LineEndpoint (LineEndpoint.chooseBtn) that, when clicked, changes a variable in MplFig (MplFig.waitingForPt) from None to the value of ptNum which is passed as an argument of LineEndpoint (in the case of linePt1, this value is 1). MplFig has button press events tied to the method MplFig.onClick() which, is MplFig.onClick is not None, passes the coordinates of the mouse click to the two QDoubleSpinBox objects in LineEndpoint.ptXSpin and LineEndpoint.ptYSpin. To achieve this, I pass self as the parent argument when I create the MainWIndow.plotWidget object of MplFig. I set the parent as self.parent which allows me to call the LineEndpoint object as self.parent.linePt1, which from there allows me to access the spin boxes.
This seems like a round-a-bout way of doing things and I'm wondering if anybody could suggest a better way of structuring this functionality? I like the method of passing the MplFig object as an argument to the LineEndpoint class as that makes it clear from the init method in the class definition that the LineEndpoint class communicates with the MplFig class. I know I cannot have both classes depend on each other in the same way, but i would love to learn a way of doing this that still makes it clear in the code that the objects are communicating. I am still open to all suggestions though!
from PyQt5.QtWidgets import (
QMainWindow, QApplication, QLabel, QLineEdit, QPushButton, QFileDialog,
QWidget, QHBoxLayout, QVBoxLayout, QMessageBox, QListWidget,
QAbstractItemView, QDoubleSpinBox
)
from PyQt5.QtCore import Qt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import (
FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
)
import sys # need sys to pass argv to QApplication
class MplFig(FigureCanvasQTAgg):
def __init__(self, parent):
self.fig = Figure()
super().__init__(self.fig)
self.parent = parent
self.waitingForPt = None
self.fig.canvas.mpl_connect('button_press_event', self.onClick)
self.ax = self.figure.add_subplot(111)
def onClick(self, e):
if self.waitingForPt is not None:
if self.waitingForPt == 1:
lineObj = self.parent.linePt1
roundX = round(e.xdata, lineObj.ptPrec)
roundY = round(e.ydata, lineObj.ptPrec)
print(f'x{self.waitingForPt}: {roundX}, '
f'y{self.waitingForPt}: {roundY}'
)
lineObj.ptXSpin.setValue(roundX)
lineObj.ptYSpin.setValue(roundY)
lineObj.chooseBtn.setStyleSheet(
'background-color: light gray'
)
self.waitingForPt = None
class LineEndpoint(QWidget):
def __init__(self, parent, mplObject, ptNum, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.mpl = mplObject
self.layout = QVBoxLayout()
row0Layout = QHBoxLayout()
ptXLabel = QLabel(f'X{ptNum}:')
row0Layout.addWidget(ptXLabel)
ptMin = 0
ptMax = 1000
ptStep = 1
self.ptPrec = 2
self.ptXSpin = QDoubleSpinBox()
self.ptXSpin.setSingleStep(ptStep)
self.ptXSpin.setMinimum(ptMin)
self.ptXSpin.setMaximum(ptMax)
self.ptXSpin.setDecimals(self.ptPrec)
row0Layout.addWidget(self.ptXSpin)
ptYLabel = QLabel(f'Y{ptNum}:')
row0Layout.addWidget(ptYLabel)
self.ptYSpin = QDoubleSpinBox()
self.ptYSpin.setMinimum(ptMin)
self.ptYSpin.setMaximum(ptMax)
self.ptYSpin.setSingleStep(ptStep)
self.ptYSpin.setDecimals(self.ptPrec)
row0Layout.addWidget(self.ptYSpin)
self.layout.addLayout(row0Layout)
row1Layout = QHBoxLayout()
self.chooseBtn = QPushButton('Choose on Plot')
self.chooseBtn.clicked.connect(lambda: self.chooseBtnClicked(ptNum))
row1Layout.addWidget(self.chooseBtn)
self.layout.addLayout(row1Layout)
def chooseBtnClicked(self, endpointNum):
print(f'Choosing point {endpointNum}...')
self.chooseBtn.setStyleSheet('background-color: red')
self.mpl.waitingForPt = endpointNum
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setLayouts()
def setLayouts(self):
self.sideBySideLayout = QHBoxLayout()
self.plotWidget = MplFig(self)
self.sideBySideLayout.addWidget(self.plotWidget)
self.linePt1 = LineEndpoint(self, self.plotWidget, 1)
self.sideBySideLayout.addLayout(self.linePt1.layout)
mainContainer = QWidget()
mainContainer.setLayout(self.sideBySideLayout)
self.setCentralWidget(mainContainer)
QApp = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(QApp.exec_())
If you want to transmit information between objects (remember that classes are only abstractions) then you must use signals:
import sys
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QApplication,
QDoubleSpinBox,
QGridLayout,
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QWidget,
)
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
class MplFig(FigureCanvasQTAgg):
clicked = pyqtSignal(float, float)
def __init__(self, parent=None):
super().__init__(Figure())
self.setParent(parent)
self.figure.canvas.mpl_connect("button_press_event", self.onClick)
self.ax = self.figure.add_subplot(111)
def onClick(self, e):
self.clicked.emit(e.xdata, e.ydata)
class LineEndpoint(QWidget):
def __init__(self, ptNum, parent=None):
super().__init__(parent)
ptMin = 0
ptMax = 1000
ptStep = 1
ptPrec = 2
self.ptXSpin = QDoubleSpinBox(
singleStep=ptStep, minimum=ptMin, maximum=ptMax, decimals=ptPrec
)
self.ptYSpin = QDoubleSpinBox(
singleStep=ptStep, minimum=ptMin, maximum=ptMax, decimals=ptPrec
)
self.chooseBtn = QPushButton("Choose on Plot", checkable=True)
self.chooseBtn.setStyleSheet(
"""
QPushButton{
background-color: light gray
}
QPushButton:checked{
background-color: red
}"""
)
lay = QGridLayout(self)
lay.addWidget(QLabel(f"X{ptNum}"), 0, 0)
lay.addWidget(self.ptXSpin, 0, 1)
lay.addWidget(QLabel(f"Y{ptNum}"), 0, 2)
lay.addWidget(self.ptYSpin, 0, 3)
lay.addWidget(self.chooseBtn, 1, 0, 1, 4)
lay.setRowStretch(lay.rowCount(), 1)
#pyqtSlot(float, float)
def update_point(self, x, y):
if self.chooseBtn.isChecked():
self.ptXSpin.setValue(x)
self.ptYSpin.setValue(y)
self.chooseBtn.setChecked(False)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setLayouts()
def setLayouts(self):
self.plotWidget = MplFig()
self.linePt1 = LineEndpoint(1)
self.plotWidget.clicked.connect(self.linePt1.update_point)
mainContainer = QWidget()
lay = QHBoxLayout(mainContainer)
lay.addWidget(self.plotWidget)
lay.addWidget(self.linePt1)
self.setCentralWidget(mainContainer)
QApp = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(QApp.exec_())
I know you can set the alignment of the text using setAlignment(), but that has no effect on the placeholder text. Maybe you would need to edit the styleSheet of the underlying documentin order to do it? Or is the document only relevant for the actual text but not the placeholder?
Here's an MWE to play around with:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QApplication, QGridLayout, QTextEdit
class Test(QDialog):
def __init__(self):
super().__init__()
self.edit = QTextEdit()
self.edit.setPlaceholderText(
# '<html><body><p align="center">'
'this is a placeholder'
# '</p></body></html>'
)
self.edit.setAlignment(Qt.AlignHCenter)
self.lay = QGridLayout(self)
self.lay.addWidget(self.edit)
self.setLayout(self.lay)
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = Test()
GUI.show()
sys.exit(app.exec_())
The placeholder cannot be directly customized, as it is directly painted by the QTextEdit using the default top alignment, so the only solution is to subclass, overwrite the paintEvent and paint the placeholder using your own alignment.
You might also add much more control by using a QTextDocument, which will allow you to use html and custom colors/alignment/etc.
class HtmlPlaceholderTextEdit(QTextEdit):
_placeholderText = ''
def setPlaceholderText(self, text):
if Qt.mightBeRichText(text):
self._placeholderText = QTextDocument()
try:
color = self.palette().placeholderText().color()
except:
# for Qt < 5.12
color = self.palette().windowText().color()
color.setAlpha(128)
# IMPORTANT: the default stylesheet _MUST_ be set *before*
# adding any text, otherwise it won't be used.
self._placeholderText.setDefaultStyleSheet(
'''body {{color: rgba({}, {}, {}, {});}}
'''.format(*color.getRgb()))
self._placeholderText.setHtml(text)
else:
self._placeholderText = text
self.update()
def placeholderText(self):
return self._placeholderText
def paintEvent(self, event):
super().paintEvent(event)
if self.document().isEmpty() and self.placeholderText():
qp = QPainter(self.viewport())
margin = self.document().documentMargin()
r = QRectF(self.viewport().rect()).adjusted(margin, margin, -margin, -margin)
text = self.placeholderText()
if isinstance(text, str):
try:
color = self.palette().placeholderText().color()
except:
# for Qt < 5.12
color = self.palette().windowText().color()
color.setAlpha(128)
qp.setPen(color)
qp.drawText(r, self.alignment() | Qt.TextWordWrap, text)
else:
text.setPageSize(self.document().pageSize())
text.drawContents(qp, r)
class Test(QDialog):
def __init__(self):
super().__init__()
self.edit = HtmlPlaceholderTextEdit()
self.edit.setPlaceholderText(
'<html><body><p align="center">'
'this is a <font color="blue">placeholder</font>'
'</p></body></html>'
)
# ...