Difficulty Getting PyQt GUI Elements to Correctly Show - python

New Python programmer here. I'm trying to make a simple-ish GUI and cannot get one of the PyQt elements to display. Perhaps someone here can point me in the right direction and give some general code comments. Don't worry. I know my code is probably awful. We've all got to start somewhere.
I have a GUI which consists of two widgets in an HBoxLayout. One widget is a simple PushButton, the other is a custom ControlWidget. I can get these to display just fine (alignment issues notwithstanding). On the ControlWidget, I have a GridLayout with some Labels, ComboBoxes, and a subclassed QTextBrowser (Debugger). This is what I can't get to appear.
Admittedly, I'm not absolutely sure how to do this. Is the inheritance wrong? Do I need to pass something else into the lower classes? etc. I want the various elements broken up into separate files and for future events to be accessible in other parts of the code, but obviously I'm missing something.
MainGUI.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from ControlWidget import ControlWidget
# from MenuBar import MenuBar
from Debugger import Debugger
# Main
class TopWindow(QMainWindow):
def __init__(self):
super(TopWindow, self).__init__()
self.setWindowTitle('Test GUI')
self.setGeometry(0, 0, 800, 600)
self.setMaximumSize(1024, 768)
self.initUI()
def initUI(self):
# MenuBar.initMenuBar(self)
centralWidget = QWidget(self)
self.setCentralWidget(centralWidget)
hLayout = QHBoxLayout(centralWidget)
pushButton = QPushButton("Button A")
hLayout.addWidget(pushButton)
hLayout.addWidget(ControlWidget())
self.show()
# Program Entry Point
if __name__ == '__main__':
applicationInstance = QApplication(sys.argv)
ex = TopWindow()
sys.exit(applicationInstance.exec_())
ControlWidget.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from Debugger import Debugger
class ControlWidget(QWidget):
def __init__(self, parent=None):
super(ControlWidget, self).__init__(parent)
self.left = 100
self.top = 100
self.width = 320
self.height = 100
self.numClicks = 0
self.setMaximumWidth(240)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.initUI()
def initUI(self):
self.setGeometry(self.left, self.top, self.width, self.height)
self.createGridLayout()
def createGridLayout(self):
# Create Grid Layout
layout = QGridLayout()
self.setLayout(layout)
layout.setColumnStretch(0, 2)
layout.setColumnStretch(1, 3)
layout.setColumnStretch(2, 1)
# Instantiate Labels
labelA = QLabel()
labelB = QLabel()
labelC = QLabel()
labelD = QLabel()
labelE = QLabel()
labelF = QLabel()
self.labelFa = QLabel()
labelA.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
labelB.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
labelC.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
labelD.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
labelE.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
labelF.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.labelFa.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
labelA.setText('ABCD: ')
labelB.setText('BCDE: ')
labelC.setText('CDEF: ')
labelD.setText('DEFG: ')
labelE.setText('EFGH: ')
labelF.setText('FGHI: ')
# Instantiate Combo Boxes
comboBoxA = QComboBox()
comboBoxB = QComboBox()
comboBoxC = QComboBox()
comboBoxD = QComboBox()
comboBoxE = QComboBox()
comboBoxA.addItems(["A", "B", "C", "D"])
comboBoxB.addItems(["B", "C", "D", "E"])
comboBoxC.addItems(["C", "D", "E", "F"])
comboBoxD.addItems(["D", "E", "F", "G"])
comboBoxE.addItems(["E", "F", "G", "H"])
# Instantiate Push Buttons
pushButtonF = QPushButton()
pushButtonF.setText('Set Value')
# Spacer
spacer = QSpacerItem(10, 30, QSizePolicy.Fixed, QSizePolicy.Fixed)
# Message Box
labelDebug = QLabel()
labelDebug.setText("Debug")
labelDebug.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
debug = Debugger(self) # DOES NOT WORK
# Add to Grid Layout (item, row, column, rowspan, colspan)
layout.addWidget(labelA, 0, 0)
layout.addWidget(labelB, 1, 0)
layout.addWidget(labelC, 2, 0)
layout.addWidget(labelD, 3, 0)
layout.addWidget(labelE, 4, 0)
layout.addWidget(labelF, 5, 0)
layout.addWidget(comboBoxA, 0, 1, 1, 2)
layout.addWidget(comboBoxB, 1, 1, 1, 2)
layout.addWidget(comboBoxC, 2, 1, 1, 2)
layout.addWidget(comboBoxD, 3, 1, 1, 2)
layout.addWidget(comboBoxE, 4, 1, 1, 2)
layout.addWidget(pushButtonF, 5, 1, 1, 1)
layout.addWidget(self.labelFa, 5, 2, 1, 1)
layout.addItem(spacer, 6, 0, 1, 3)
layout.addWidget(labelDebug, 7, 0)
layout.addWidget(debug, 8, 0)
# Hook Up ComboBox Signals to Handlers
comboBoxA.currentIndexChanged.connect(self.comboBoxAHandler)
# Hook Up PushButton Signals to Handlers
pushButtonF.clicked.connect(self.pushButtonFHandler)
#self.horizontalGroupBox.setLayout(layout)
def comboBoxAHandler(self, i):
print('Combo Box A Selection Changed to {0}'.format(i))
#Debugger.write(self, 'Combo Box A Selection Changed')
def pushButtonFHandler(self):
print('Push Button F Clicked')
self.numClicks = self.numClicks + 1
self.labelFa.setText(str(self.numClicks))
Debugger.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Debugger(QWidget):
def __init__(self, parent=None):
super(Debugger, self).__init__(parent)
self.setMaximumWidth(240)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
self.createTextBrowser()
def createTextBrowser(self):
self.textBrowser = QTextBrowser()
self.textBrowser.setReadOnly(True)
def write(self, text):
self.textBrowser.append("• " + text)
What's the simple thing that I'm missing or doing incorrectly?

You have 2 errors:
You are not using the inheritance in Debugger, but a composition.
If you are going to want to use a variable in other methods of the same class you must make that variable member of the class.
Considering the above, the solution is:
Debugger.py
import sys
from PyQt5.QtWidgets import QTextBrowser, QSizePolicy
class Debugger(QTextBrowser):
def __init__(self, parent=None):
super(Debugger, self).__init__(parent)
self.setMaximumWidth(240)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
self.setReadOnly(True)
def write(self, text):
self.append("• " + text)
ControlWidget.py
# ...
class ControlWidget(QWidget):
# ...
def initUI(self):
# ...
# Message Box
labelDebug = QLabel()
labelDebug.setText("Debug")
labelDebug.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.debug = Debugger() # <---
# ...
layout.addWidget(labelDebug, 7, 0)
layout.addWidget(self.debug, 8, 0) # <---
# Hook Up ComboBox Signals to Handlers
# ...
def comboBoxAHandler(self, i):
print('Combo Box A Selection Changed to {0}'.format(i))
self.debug.write('Combo Box A Selection Changed') # <---
# ...
I recommend reading the following publications so that you understand the difference between inheritance(is-a) and composition(has-a):
Difference between Inheritance and Composition
Python: Inheritance versus Composition

My guess is that it has to do with the fact that your textbrowser object doesn't have a parent, and you never explicitly told it to .show().
Usually, when a widget has a parent, a call to the parent widget's show method will show the children as well. Since your textbrowser object has no parent, the alternative is to show that object explicitly, but you haven't done that either.
In your TopWindow class' initUI method, you call self.show(). This is the actual invokation that shows all of the parent's children (the parent being self, the window). This single call correctly shows your other widgets because you explicitly assigned the main window as their parent, but it does not show your textbrowser because you did not give it a parent. (note, because your debugger class is using composition as opposed to inheritance, you did give the debugger a parent (which is a QWidget), but the actual QTextBrowser object does not have a parent)

Related

Changing QLineEdit with the push of a button

I want to do now is find a way to increment the QLineEdit box (Step_Box) with the corresponding value of one of the buttons (0.01, 1, 10...). Each time I press on one of the buttons, the entry on QLineEdit must increment by that value. How should I do this?
# -*- coding: utf8 -*-
import sys
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QGridLayout,
QPushButton,
QLineEdit,
QLabel,
QComboBox,
)
class XY_Buttons(QPushButton):
def __init__(self, name):
super(XY_Buttons, self).__init__()
self.clicked.connect(self.is_clicked)
self.setText(str(name))
def is_clicked(self):
print("A")
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("Suruga Controller")
OuterLayout = QGridLayout() # Parent layout
ArrowsLayout = QGridLayout() # Arrows layout -- in here are the XY controller
UpDownLayout = QGridLayout() # Up and Down layout -- z controller
XYLayout = QGridLayout() # XY options layout -- step size and others
ZLayout = QGridLayout() # Z options layout -- step size and others
# Adding Widgets for ArrowsLayout
ArrowsLayout.addWidget(QPushButton("Up"), 0, 1)
ArrowsLayout.addWidget(QPushButton("Down"), 2, 1)
ArrowsLayout.addWidget(QPushButton("Left"), 1, 0)
ArrowsLayout.addWidget(QPushButton("Right"), 1, 2)
# Adding Widgets for XYLayout
xstep=0.0
Step_Box = QLineEdit(str(xstep))
Step_Box.setReadOnly(True)
Units_Box = QComboBox()
Units_Box.addItems([u"10E-6 (µm) ", "10E-3 (cm)"])
XYLayout.addWidget(XY_Buttons(0.01), 1, 0)
XYLayout.addWidget(XY_Buttons(0.1), 1, 1)
XYLayout.addWidget(XY_Buttons(1), 1, 2)
XYLayout.addWidget(XY_Buttons(10), 2, 0)
XYLayout.addWidget(XY_Buttons(100), 2, 1)
XYLayout.addWidget(XY_Buttons(1000), 2, 2)
# XYLayout.addWidget(Clear_Button(), 0, 2, 1, 2)
XYLayout.addWidget(QLabel("Step is:"), 0, 0, 1, 2)
XYLayout.addWidget(Step_Box, 0, 1)
XYLayout.addWidget(Units_Box, 3, 1)
# Nesting all layouts
OuterLayout.addLayout(ArrowsLayout, 0, 0)
OuterLayout.addLayout(XYLayout, 0, 1)
OuterLayout.addLayout(UpDownLayout, 1, 0)
OuterLayout.addLayout(ZLayout, 1, 1)
widget = QWidget()
widget.setLayout(OuterLayout)
self.setCentralWidget(widget)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
You need some reference to the instance of QLineEdit i.e. Step_Box in order to change their values from the button click, since you have a custom button class, you can just pass the instance of the line edit and store it there, then you can just increment the value from the slot to which you have connected the clicked signal of the buttons.
import sys
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QGridLayout,
QPushButton,
QLineEdit,
QLabel,
QComboBox,
)
class XY_Buttons(QPushButton):
def __init__(self, name, lineEdit:QLineEdit=None):
super(XY_Buttons, self).__init__()
self.clicked.connect(self.is_clicked)
self.setText(str(name))
self.lineEdit = lineEdit
def is_clicked(self):
print("A")
self.lineEdit.setText(str(float(self.lineEdit.text())+ float(self.text())))
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("Suruga Controller")
OuterLayout = QGridLayout() # Parent layout
ArrowsLayout = QGridLayout() # Arrows layout -- in here are the XY controller
UpDownLayout = QGridLayout() # Up and Down layout -- z controller
XYLayout = QGridLayout() # XY options layout -- step size and others
ZLayout = QGridLayout() # Z options layout -- step size and others
# Adding Widgets for ArrowsLayout
ArrowsLayout.addWidget(QPushButton("Up"), 0, 1)
ArrowsLayout.addWidget(QPushButton("Down"), 2, 1)
ArrowsLayout.addWidget(QPushButton("Left"), 1, 0)
ArrowsLayout.addWidget(QPushButton("Right"), 1, 2)
# Adding Widgets for XYLayout
xstep=0.0
Step_Box = QLineEdit(str(xstep))
Step_Box.setReadOnly(True)
Units_Box = QComboBox()
Units_Box.addItems([u"10E-6 (µm) ", "10E-3 (cm)"])
XYLayout.addWidget(XY_Buttons(0.01, Step_Box), 1, 0)
XYLayout.addWidget(XY_Buttons(0.1, Step_Box), 1, 1)
XYLayout.addWidget(XY_Buttons(1, Step_Box), 1, 2)
XYLayout.addWidget(XY_Buttons(10, Step_Box), 2, 0)
XYLayout.addWidget(XY_Buttons(100, Step_Box), 2, 1)
XYLayout.addWidget(XY_Buttons(1000, Step_Box), 2, 2)
# XYLayout.addWidget(Clear_Button(), 0, 2, 1, 2)
XYLayout.addWidget(QLabel("Step is:"), 0, 0, 1, 2)
XYLayout.addWidget(Step_Box, 0, 1)
XYLayout.addWidget(Units_Box, 3, 1)
# Nesting all layouts
OuterLayout.addLayout(ArrowsLayout, 0, 0)
OuterLayout.addLayout(XYLayout, 0, 1)
OuterLayout.addLayout(UpDownLayout, 1, 0)
OuterLayout.addLayout(ZLayout, 1, 1)
widget = QWidget()
widget.setLayout(OuterLayout)
self.setCentralWidget(widget)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
But generally, passing reference like this is not considered a good practice, so it'd have been better if you connected it from within the class where the instance was created.
I would advise to introduce a clear separation of the data, i.e. the currently selected step, from the GUI. A common approach is Model-View-Controller (MVC).
I think it is necessary for any GUI application, as it otherwise leads to spaghetti code of widgets talking to each other, patching out bugs.
There are several View and Model components already available inside Qt5 (QStringListModel + QListView for example), but it's trivial to make our own.
Here I introduce the FooModel and the corresponding view, along with buttons (Controller) that attempts to modify the model.
# -*- coding: utf8 -*-
import sys
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QGridLayout,
QPushButton,
QLineEdit,
QLabel,
QComboBox,
)
class FooChanger(QPushButton):
# Note: This is the controller. It doesn't try to be clever and modify the view.
# It just modifies the model. If the model wishes to signal to any view component, that's the models responsibility.
def __init__(self, foo_model, value):
super().__init__(str(value))
self.clicked.connect(self.is_clicked)
self.model = foo_model
self.value = value
def is_clicked(self):
self.model.set_value(self.value)
class FooView(QLabel):
# This is the view. It does try to be clever either. It just updates information when the model signals.
def __init__(self, model):
super().__init__("hejsan")
self.model = model
self.model.data_changed.connect(self.refresh)
self.refresh() # initial refresh
def refresh(self):
self.setText(str(self.model.value))
class FooModel(QObject):
# This is our simple model. It only has a signal, and uses it to indicate when the data has been modified.
# Make sure to use the set_value function and don't poke at self.value directly!
data_changed = pyqtSignal()
def __init__(self, initial_value):
super().__init__()
# We are currently just storing one piece of information. The infamous Foo value!
self.value = initial_value
def set_value(self, new_value):
self.value = new_value
self.data_changed.emit()
class MainWindow(QMainWindow):
def __init__(self, model):
super().__init__()
self.setWindowTitle("Suruga Controller")
OuterLayout = QGridLayout() # Parent layout
ArrowsLayout = QGridLayout() # Arrows layout -- in here are the XY controller
UpDownLayout = QGridLayout() # Up and Down layout -- z controller
XYLayout = QGridLayout() # XY options layout -- step size and others
ZLayout = QGridLayout() # Z options layout -- step size and others
# Adding Widgets for ArrowsLayout
ArrowsLayout.addWidget(QPushButton("Up"), 0, 1)
ArrowsLayout.addWidget(QPushButton("Down"), 2, 1)
ArrowsLayout.addWidget(QPushButton("Left"), 1, 0)
ArrowsLayout.addWidget(QPushButton("Right"), 1, 2)
# Adding Widgets for XYLayout
step_box = FooView(model)
unix_box = QComboBox()
unix_box.addItems([u"10E-6 (µm) ", "10E-3 (cm)"])
XYLayout.addWidget(FooChanger(model, 0.01), 1, 0)
XYLayout.addWidget(FooChanger(model, 0.1), 1, 1)
XYLayout.addWidget(FooChanger(model, 1), 1, 2)
XYLayout.addWidget(FooChanger(model, 10), 2, 0)
XYLayout.addWidget(FooChanger(model, 100), 2, 1)
XYLayout.addWidget(FooChanger(model, 1000), 2, 2)
XYLayout.addWidget(QLabel("Step is:"), 0, 0, 1, 2)
XYLayout.addWidget(step_box, 0, 1)
XYLayout.addWidget(unix_box, 3, 1)
# Nesting all layouts
OuterLayout.addLayout(ArrowsLayout, 0, 0)
OuterLayout.addLayout(XYLayout, 0, 1)
OuterLayout.addLayout(UpDownLayout, 1, 0)
OuterLayout.addLayout(ZLayout, 1, 1)
widget = QWidget()
widget.setLayout(OuterLayout)
self.setCentralWidget(widget)
app = QApplication(sys.argv)
foo = FooModel(3.1415)
window = MainWindow(foo) # The view acts on our foo.
window.show()
app.exec()
There is no need for cross-talk between widgets here. It's just a view that only displays current data (it doesn't even care how the data got modified, it just always works), a controller that just informs the model that the user requested a change, and a model that is blissfully ignorant of the existence of either widgets or buttons.

Animation to hide rows in a

I'm trying to create an animation to hide unselected rows in a QTableWidget.
I want the height of the rows to decrease until it is 0, for a smoother transition.
I found the following in C++ that does exactly what I'm trying to achieve, but I cannot get the same result in Python: https://forum.qt.io/topic/101437/animate-qtablewidget/4
I noticed they use Q_PROPERTY, but I honestly do not understand how it works...
Here what I came up for now (the animation is not working):
`
import sys
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from PySide2.QtCore import *
class Example(QWidget):
def __init__(self):
super().__init__()
self.resize(600,200)
self.initUI()
def initUI(self):
layout = QHBoxLayout()
self.table = QTableWidget()
self.table.setColumnCount(3)
tableLabels = ["First Name", "Surname", "Age"]
self.table.setHorizontalHeaderLabels(tableLabels)
self.table.setRowCount(3)
self.table.verticalHeader().setMinimumSectionSize(1)
users = {
'0': ["peter", "parker", "19"],
'1': ["bruce", "banner", "42"],
'2': ["barry", "allen", "35"]
}
for row, data in users.items():
for column, value in enumerate(data):
self.table.setItem(int(row), column, QTableWidgetItem(value))
button1 = QPushButton("Hide")
button1.clicked.connect(lambda: self.hide_row())
layout.addWidget(self.table)
layout.addWidget(button1)
self.setLayout(layout)
self.show()
def hide_row(self):
for i in [x for x in range(self.table.rowCount()) if x != self.table.currentRow()]:
self.rowHeight = QPropertyAnimation(self.table.item(i, 0), b"geometry")
self.rowHeight.setDuration(400)
self.rowHeight.setStartValue(QRect(0, 0, 0, self.table.rowHeight(i)))
self.rowHeight.setEndValue(QRect(0, 0, 0, 0))
self.rowHeight.start()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
`
Any idea how to achieve this goal in Python?
A Qt animation requires that the parent (or target, in the case of a Property Animation) inherits from QObject, and table widget items do not.
Also, the geometry property should exist for the target object, and items do not have such a property.
The solution is to use a QVariantAnimation and use the valueChanged signal to set the row height on the table.
class Example(QWidget):
def __init__(self):
super().__init__()
self.resize(600,200)
self.initUI()
self.animation = QVariantAnimation(self)
self.animation.setDuration(400)
self.animation.valueChanged.connect(self.animationChanged)
self.animation.finished.connect(self.finished)
def animationChanged(self, height):
self.table.setRowHeight(self.currentRow, height)
def finished(self):
self.table.setRowHidden(self.currentRow, True)
# reset the height, so that we can use it as a "stored" value in case
# we want to show the row again
self.table.setRowHeight(self.currentRow, self.currentHeight)
def hide_row(self):
# ignore if the animation is already resizing a row
if self.animation.state():
return
self.currentRow = self.table.currentRow()
self.currentHeight = self.table.rowHeight(self.currentRow)
self.animation.setStartValue(self.currentHeight)
self.animation.setEndValue(1)
self.animation.start()
# ...

QLineEdit in QTableWdiget cell not losing focus

I'm currently trying to make a graphical interface using PyQt5. Everything has been working fine until now (I have some experience in this after all), but I've been struggling with this problem for a few days now and I can't find a solution anywhere.
In my app I display a FigureCanvas from matplotlib (along with various other widgets) where I have a key event connected to a function which picks a point from a mouse click and saves this point in a QTableWidget. However, I want the user to be able to edit the y-value that was picked up using a validator, so I changed the second column from holding a QTableWidgetItem to a QLineEdit widget. Everything works except when I press enter after editing the QLineEdit widget in the table, the focus doesn't change as expected (I set the focus to go back to the canvas but the cell stays active with the cursor blinking).
In the same app I have another QLineEdit widget elsewhere in the layout which works as expected. When I press enter in this box, the function connected to its returnPressed signal sends the focus back to the canvas.
Can anyone please help me figure out what is going on?
Also, I don't understand why the app start up with focus being on the QLineEdit when I explicitly set focus to the canvas while creating the main frame. Do you know how to fix this?
I've stripped back my app to a minimal example pasted below and the behavior remains the same.
Just to summarize:
I click on the canvas and press 'a' which call the function 'add_point'.
I then click a point in the plot which creates a new row in the Widget and sets focus to the second column.
I input whatever float value I want here and press Enter.
The function 'print_val' is then called but the focus doesn't return to the canvas as expected. The cursor stays blinking in the table cell.
When I edit the leftmost QLineEdit (outside the table) and hit Enter, the function 'func' is called and correctly sends the focus back to the canvas.
Why do the two instances of QLineEdit behave differently?
For reference, the output of the function 'print_val' gives me:
TableWidget has focus? False
Cell editor has focus? False
Thanks in advance!
import sys
from matplotlib.backends.backend_qt5agg import FigureCanvas, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class GraphicInterface(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setWindowTitle('Test')
self._main = QWidget()
self.setCentralWidget(self._main)
# Create Toolbar and Menubar
toolbar = QToolBar()
toolbar.setMovable(False)
toolbar_fontsize = QFont()
toolbar_fontsize.setPointSize(14)
quit_action = QAction("Close", self)
quit_action.setFont(toolbar_fontsize)
quit_action.setStatusTip("Quit the application")
quit_action.triggered.connect(self.close)
quit_action.setShortcut("ctrl+Q")
toolbar.addAction(quit_action)
# =============================================================
# Start Layout:
layout = QHBoxLayout(self._main)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.line_editor = QLineEdit("3")
self.line_editor.setValidator(QIntValidator(1, 100))
self.line_editor.returnPressed.connect(self.func)
layout.addWidget(self.line_editor)
# Create Table for Pixel Identifications:
pixtab_layout = QVBoxLayout()
label_pixtab_header = QLabel("Pixel Table\n")
label_pixtab_header.setAlignment(Qt.AlignCenter)
self.linetable = QTableWidget()
self.linetable.verticalHeader().hide()
self.linetable.setColumnCount(2)
self.linetable.setHorizontalHeaderLabels(["Pixel", "Wavelength"])
self.linetable.setColumnWidth(0, 80)
self.linetable.setColumnWidth(1, 90)
self.linetable.setFixedWidth(180)
pixtab_layout.addWidget(label_pixtab_header)
pixtab_layout.addWidget(self.linetable)
layout.addLayout(pixtab_layout)
# Create Figure Canvas:
right_layout = QVBoxLayout()
self.fig = Figure(figsize=(6, 6))
self.canvas = FigureCanvas(self.fig)
self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.canvas.updateGeometry()
self.canvas.mpl_connect('key_press_event', self.on_key_press)
self.canvas.setFocusPolicy(Qt.StrongFocus)
self.canvas.setFocus()
right_layout.addWidget(self.canvas)
self.mpl_toolbar = NavigationToolbar(self.canvas, self)
right_layout.addWidget(self.mpl_toolbar)
layout.addLayout(right_layout)
# Plot some random data:
self.ax = self.fig.add_subplot(111)
self.ax.plot([0, 1, 5, 10, 20], [-1, 2, -4, 5, -2])
def on_key_press(self, e):
if e.key == "a":
self.add_point()
def add_point(self):
print("Select point...")
point = self.fig.ginput(1)
x0, _ = point[0]
rowPosition = self.linetable.rowCount()
self.linetable.insertRow(rowPosition)
item = QTableWidgetItem("%.2f" % x0)
item.setFlags(Qt.ItemIsEnabled)
item.setBackground(QColor('lightgray'))
self.linetable.setItem(rowPosition, 0, item)
y_item = QLineEdit("")
y_item.setValidator(QDoubleValidator())
y_item.returnPressed.connect(self.print_val)
self.linetable.setCellWidget(rowPosition, 1, y_item)
self.linetable.cellWidget(rowPosition, 1).setFocus()
def print_val(self):
rowPosition = self.linetable.rowCount()
editor = self.linetable.cellWidget(rowPosition-1, 1)
y_in = float(editor.text())
print(" Table value: %.2f" % y_in)
# and set focus back to canvas:
self.canvas.setFocus()
# Check which widget has focus:
focus_widget = self.focusWidget()
print(focus_widget.__class__)
print("TableWidget has focus? %r" % self.linetable.hasFocus())
print("Cell editor has focus? %r" % editor.hasFocus())
def func(self):
# dome some stuff
print(self.line_editor.text())
# and set focus back to canvas:
self.canvas.setFocus()
if __name__ == '__main__':
# Launch App:
qapp = QApplication(sys.argv)
app = GraphicInterface()
app.show()
qapp.exec_()
If you want to validate the information of a QTableWidget it is not necessary to insert a QLineEdit but use the one created by the delegate and set a QValidator there:
class ValidatorDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = super(ValidatorDelegate, self).createEditor(parent, option, index)
if isinstance(editor, QtWidgets.QLineEdit):
editor.setValidator(QtGui.QDoubleValidator(editor))
return editor
And then open the editor with the edit method of the view, to change the focus you can use the delegate's closeEditor signal:
import sys
from matplotlib.backends.backend_qt5agg import (
FigureCanvas,
NavigationToolbar2QT as NavigationToolbar,
)
from matplotlib.figure import Figure
from PyQt5 import QtCore, QtGui, QtWidgets
class ValidatorDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = super(ValidatorDelegate, self).createEditor(parent, option, index)
if isinstance(editor, QtWidgets.QLineEdit):
editor.setValidator(QtGui.QDoubleValidator(editor))
return editor
class GraphicInterface(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(GraphicInterface, self).__init__(parent)
self.setWindowTitle("Test")
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
# Create Toolbar and Menubar
toolbar = QtWidgets.QToolBar(movable=False)
toolbar_fontsize = QtGui.QFont()
toolbar_fontsize.setPointSize(14)
quit_action = QtWidgets.QAction("Close", self)
quit_action.setFont(toolbar_fontsize)
quit_action.setStatusTip("Quit the application")
quit_action.triggered.connect(self.close)
quit_action.setShortcut("ctrl+Q")
toolbar.addAction(quit_action)
self.line_editor = QtWidgets.QLineEdit("3")
self.line_editor.setValidator(QtGui.QIntValidator(1, 100))
label_pixtab_header = QtWidgets.QLabel(
"Pixel Table\n", alignment=QtCore.Qt.AlignCenter
)
self.linetable = QtWidgets.QTableWidget()
self.linetable.verticalHeader().hide()
self.linetable.setColumnCount(2)
self.linetable.setHorizontalHeaderLabels(["Pixel", "Wavelength"])
self.linetable.setColumnWidth(0, 80)
self.linetable.setColumnWidth(1, 90)
self.linetable.setFixedWidth(180)
delegate = ValidatorDelegate(self.linetable)
self.linetable.setItemDelegate(delegate)
layout = QtWidgets.QHBoxLayout(self._main)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.line_editor)
pixtab_layout = QtWidgets.QVBoxLayout()
pixtab_layout.addWidget(label_pixtab_header)
pixtab_layout.addWidget(self.linetable)
layout.addLayout(pixtab_layout)
# Create Figure Canvas:
right_layout = QtWidgets.QVBoxLayout()
self.fig = Figure(figsize=(6, 6))
self.canvas = FigureCanvas(self.fig)
self.canvas.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
self.canvas.updateGeometry()
self.canvas.mpl_connect("key_press_event", self.on_key_press)
self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus)
self.canvas.setFocus()
right_layout.addWidget(self.canvas)
self.mpl_toolbar = NavigationToolbar(self.canvas, self)
right_layout.addWidget(self.mpl_toolbar)
layout.addLayout(right_layout)
delegate.closeEditor.connect(self.canvas.setFocus)
# Plot some random data:
self.ax = self.fig.add_subplot(111)
self.ax.plot([0, 1, 5, 10, 20], [-1, 2, -4, 5, -2])
def on_key_press(self, e):
if e.key == "a":
self.add_point()
def add_point(self):
print("Select point...")
point = self.fig.ginput(1)
if not point:
return
x0, _ = point[0]
row = self.linetable.rowCount()
self.linetable.insertRow(row)
item = QtWidgets.QTableWidgetItem("%.2f" % x0)
item.setFlags(QtCore.Qt.ItemIsEnabled)
item.setBackground(QtGui.QColor("lightgray"))
self.linetable.setItem(row, 0, item)
index = self.linetable.model().index(row, 1)
self.linetable.edit(index)
if __name__ == "__main__":
# Launch App:
qapp = QtWidgets.QApplication(sys.argv)
app = GraphicInterface()
app.show()
qapp.exec_()

Prevent clipping the absolute positioned child widget by the parent

I'm trying to create a layout where is existing a row, and this row should contain an absolute positioned button which should be placed outside of this row.
Here is the simple schema
I did it by just pushing a child button into the parent button (I'm not sure that it's a correct solution) and moved it to some absolute coordinates.
It works but, unfortunately, the child button is clipping by the parent. So it's like overflow: hidden in CSS. But in case of QT I couldn't found how to disable this behavior.
Here is the demo of my current QUI
Is there exists any way to solve it? Or should I just use some widget combination with the empty spacer etc.?
btn = QPushButton("button")
test = QPushButton("X")
test.setParent(btn)
test.move(200, 5)
self.layout.addWidget(btn)
Full code of the UI class (minimal reproducible example)
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
btn = QPushButton("button")
test = QPushButton("X")
test.setParent(btn)
test.move(200, 5)
self.layout.addWidget(btn)
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,74)
self.layout.setSpacing(0)
# self.layout.addStretch(-1)
self.setMinimumSize(640,400)
self.setWindowFlags(Qt.FramelessWindowHint)
Sorry, but the advice of #Heike is absolutely correct and you should not look for wrong solutions.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setMinimumSize(640,400)
self.setWindowFlags(Qt.FramelessWindowHint)
btn = QPushButton("button")
test = QPushButton("X")
test.setParent(btn)
# test.move(200, 5)
# self.layout = QVBoxLayout()
self.layout = QGridLayout()
self.layout.addWidget(btn, 0, 0, 1, 10)
self.layout.addWidget(test, 0, 11, 1, 1)
self.layout.setContentsMargins(0,0,0,74)
self.layout.setSpacing(0)
self.setLayout(self.layout)
if __name__ == '__main__':
import sys
application = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(application.exec_())

How to make widgets scale with the window in PyQt5?

I'm trying to set up a GUI which will include multiple pages in PyQt5. The window will have a minimum size of 800x600, but is customisable beyond that. I want most, but not all, elements on the window to scale along with it. I have a solution already, but I feel that it is not very elegant.
Here is an example of the window at 800x600:
And here's another example after scaling
I've tried using QVBoxLayout, but with that system, I can't manage to keep the layout as it is here (not to say that it's impossible), rather the widgets all become centred and of the same width. At a later date, I might also be looking at adding widgets to the side that will be at the same y-value as some of the existing widgets, which is another thing that I'm not sure on how to do with the box layout.
Here's the relevant code:
class CreatePage(QWidget):
def __init__(self, parent=None):
super().__init__()
self.initUI()
def initUI(self):
self.homeBtn = QPushButton("Home", self)
self.homeBtn.move(10, 10)
self.frontLabel = QLabel("Front", self)
self.frontLabel.setFont(QFont("Decorative", 20))
self.frontEdit = QTextEdit(self)
self.frontEdit.setFont(QFont("Decorative", 11))
self.backLabel = QLabel("Back", self)
self.backLabel.setFont(QFont("Decorative", 20))
self.backEdit = QTextEdit(self)
self.backEdit.setFont(QFont("Decorative", 11))
def paintEvent(self, e):
qp = QPainter()
qp.setFont(QFont("Decorative", 20))
size = self.size()
h = size.height()
w = size.width()
frontW = qp.fontMetrics().width("Front")
self.frontLabel.move((w/2) - (frontW/2) , h/15)
#I use fontMetrics to determine the width of the text
#I then use this information to centre the text
self.frontEdit.move(50, h/15 + 40)
self.frontEdit.resize(w-100, h/3)
backW = qp.fontMetrics().width("Back")
self.backLabel.move((w/2) - (backW/2), h/2)
self.backEdit.move(50, h/2 + 40)
self.backEdit.resize(w-100, h/3)
Apologies for any general sloppiness, I am new to PyQt and to GUI programming on the whole. Anyway, the formulas I've used for resizing and moving widgets are quite arbitrary. Does anyone know a better way of achieving this effect?
Try it:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class CreatePage(QWidget):
def __init__(self, parent=None):
super().__init__()
self.initUI()
def initUI(self):
self.homeBtn = QPushButton("Home")
self.frontLabel = QLabel("Front")
self.frontLabel.setFont(QFont("Decorative", 20))
self.frontEdit = QTextEdit(placeholderText="frontEdit")
self.frontEdit.setFont(QFont("Decorative", 11))
self.backLabel = QLabel("Back")
self.backLabel.setFont(QFont("Decorative", 20))
self.backEdit = QTextEdit(placeholderText="backEdit")
self.backEdit.setFont(QFont("Decorative", 11))
grid = QGridLayout()
grid.addWidget(self.homeBtn, 0, 0, alignment=Qt.AlignTop | Qt.AlignLeft)
grid.addWidget(self.frontLabel, 1, 0, alignment=Qt.AlignCenter)
grid.addWidget(self.frontEdit, 2, 0)
grid.addWidget(self.backLabel, 3, 0, alignment=Qt.AlignCenter)
grid.addWidget(self.backEdit, 4, 0)
self.setLayout(grid)
if __name__=="__main__":
app = QApplication(sys.argv)
myapp = CreatePage()
myapp.show()
sys.exit(app.exec_())

Categories

Resources