how to promote widget to different classes with different selection? - python

I am using pyqt5 to plot stock trend on a widget. Sometime the widget only has one plot, sometimes I'd like to have 3 subplots. I designed two classes for same widget like below. Firstly I promote the widget to the below first class with only one plot. Later when I select different button to plot different plots, I'd like to change the plot to have 3 subplots, which means I need to change class for the widget. Is it possible to change class for same widget with different selections? or Is there any alternative way?
class gui_stock_oneplot(QWidget):
def __init__(self,parent=None):
QWidget.__init__(self,parent)
self.canvas=FigureCanvas(Figure())
vertical_layout=QVBoxLayout()
vertical_layout.addWidget(self.canvas)
self.canvas.axis1=self.canvas.figure.add_subplot(111)
self.canvas.axis2=self.canvas.axis1.twinx()
self.canvas.axis3=self.canvas.axis1.twinx()
self.canvas.figure.set_facecolor("white") #lightblue
self.setLayout(vertical_layout)
want to change widget class to:
class gui_stock_3plot(QWidget):
def __init__(self,parent=None):
QWidget.__init__(self,parent)
self.canvas=FigureCanvas(Figure())
vertical_layout=QVBoxLayout()
vertical_layout.addWidget(self.canvas)
self.canvas.axis1=self.canvas.figure.add_subplot(311)
self.canvas.axis2=self.canvas.figure.add_subplot(312)
self.canvas.axis3=self.canvas.figure.add_subplot(313)
self.canvas.figure.set_facecolor("white") #lightblue
self.setLayout(vertical_layout)

If by "promote" the OP means using a custom widget in .ui then the answer is no, you can't.
Clearly the OP has an XY problem since he is wondering about a possible solution: how to switch between 2 classes a widget that was promoted, instead of the background problem: How to change the plots in a custom widget?
For the underlying problem there are at least 2 possible solutions:
You can implement all the logic in the same widget first by cleaning the figure and then repainting:
class Widget(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.canvas = FigureCanvas(Figure())
layout.addWidget(self.canvas)
def one_plot(self):
self.canvas.figure.clear()
self.canvas.axis1 = self.canvas.figure.add_subplot(111)
self.canvas.axis2 = self.canvas.axis1.twinx()
self.canvas.axis3 = self.canvas.axis1.twinx()
self.canvas.figure.set_facecolor("white") # lightblue
self.canvas.draw()
def three_plot(self):
self.canvas.figure.clear()
self.canvas.axis1 = self.canvas.figure.add_subplot(311)
self.canvas.axis2 = self.canvas.figure.add_subplot(312)
self.canvas.axis3 = self.canvas.figure.add_subplot(313)
self.canvas.figure.set_facecolor("white") # lightblue
self.canvas.draw()
Then just use the clicked signal of each function to change plots:
self.one_button.clicked.connect(self.foo_widget.one_plot)
self.three_button.clicked.connect(self.foo_widget.three_plot)
The other solution is not to promote but to use a QStackedWidget. The logic is to add each widget to the QStackedWidget and change the index with the buttons.
self.one_widget = gui_stock_oneplot()
one_index = self.stacked_widget.addWidget(self.one_widget)
self.one_button.clicked.connect(
lambda *args, index=one_index: self.stacked_widget.setCurrentIndex(index)
)
self.three_widget = gui_stock_3plot()
three_index = self.stacked_widget.addWidget(self.three_widget)
self.three_button.clicked.connect(
lambda *args, index=three_index: self.stacked_widget.setCurrentIndex(index)
)

Related

How can i align a CellWidget of a TableWidget to the center of the Item in pyqt5

i have a comboBox inside of a tableWidget and the verticalHeader DefaultSectionSize is 60.
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self,parent)
self.table = QTableWidget()
self.setCentralWidget(self.table)
self.table.verticalHeader().setDefaultSectionSize(60)
self.table.setColumnCount(2)
self.table.setRowCount(2)
data = ["1","2"]
for i in range(2):
item = QTableWidgetItem(data[i])
self.table.setItem(i,0,item)
self.combo_sell = QComboBox()
self.combo_sell.setMaximumHeight(30)
self.table.setCellWidget(i,1,self.combo_sell)
But since i set the maximum size of the comboBox to 30, it stays in the top of the item.
I want to know if there's a way to align it to the center.
When setting an index widget, the view tries to set the widget geometry based on the visualRect() of the index. Setting a fixed dimension forces the widget to align itself to the default origin, which is usually the top left corner.
The only way to vertically center a widget with a fixed height is to use a container with a vertical box layout and add the combo to it:
for i in range(2):
item = QTableWidgetItem(data[i])
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(i,0,item)
container = QWidget()
layout = QVBoxLayout(container)
combo_sell = QComboBox()
layout.addWidget(combo_sell)
combo_sell.setMaximumHeight(30)
self.table.setCellWidget(i, 1, container)
Note: setting instance attributes in a for loop is pointless, as the reference is lost every time the cycle loops.
If you need a simple reference to the combo, you can set it as an attribute of the widget:
container.combo_sell = QComboBox()
In this way you can easily access it when required:
widget = self.table.cellWidget(row, column)
if widget and hasattr(widget, 'combo'):
combo = widget.combo
print(combo.currentIndex())
Note that the reference is created for the python wrapper of the widget, and that behavior might change in future versions of Qt. A better and safer way to achieve this would be to use a subclass, which would also allow easier access to the combo:
class TableCombo(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.combo = QComboBox()
layout.addWidget(self.combo)
self.combo.setMaximumHeight(30)
self.currentIndex = self.combo.currentIndex
self.setCurrentIndex = self.combo.setCurrentIndex
self.addItems = self.combo.addItems
# ...
combo_sell = TableCombo()
self.table.setCellWidget(i, 1, combo_sell)
# ...
combo = self.table.cellWidget(row, column)
print(combo.currentIndex())

QTreeWidget: How to change sizeHint dynamically?

I have a QTreeWidget where the TopLevelIteps are replaced by a custom widget. Said widget have its maximum and minimum Heights animated with a QStateMachine.
QTreeWidget (GIF):
Custom Widget Animation (GIF):
The problem is that the rows will not adjust its height to fit the custom widget when it expands:
Row height fixed size (GIF):
Causing overlap between the widget items instead of pushing each other away like this:
The results that I'm after (GIF):
I tried using setSizeHint() on the top level item to but it creates a big empty space between items/widgets:
Using setSizeHint()(GIF):
I'm thinking that maybe I have to implement sizeHint() but I'm not really sure what to put there. Or is there a better approach to this problem?
I would really appreciate some hints.
Example code:
# Custom Widget
class ExpandableFrame(QFrame):
def __init__(self):
QFrame.__init__(self)
# Default Properties
self.setFixedSize(200,50)
self.setStyleSheet('background-color: #2e4076; border: 2px solid black; border-radius: 10px;')
# Setup expand button
self.expandToggle = QToolButton()
self.expandToggle.setText('[]')
self.expandToggle.setMaximumSize(10,20)
self.expandToggle.setCursor(Qt.PointingHandCursor)
# Setup layout
layout = QHBoxLayout()
layout.setAlignment(Qt.AlignRight)
layout.addWidget(self.expandToggle)
self.setLayout(layout)
self.expandArea()
# Animates minimumHeight and maximumHeight
def expandArea(self):
heigth = self.height()
newHeigth = 100
machine = QStateMachine(self)
state1 = QState()
state1.assignProperty(self, b'minimumHeight', heigth)
state1.assignProperty(self, b'maximumHeight', heigth)
state2 = QState()
state2.assignProperty(self, b'minimumHeight', newHeigth)
state2.assignProperty(self, b'maximumHeight', newHeigth)
# Create animations
expandAnim = QPropertyAnimation(self, 'minimumHeight')
closeAnim = QPropertyAnimation(self, 'maximumHeight')
expandAnim.setDuration(125)
closeAnim.setDuration(125)
expandAnim.setEasingCurve(QEasingCurve.Linear)
# Create event transitions
eventTransition1 = QEventTransition(self.expandToggle, QEvent.MouseButtonPress)
eventTransition1.setTargetState(state2)
eventTransition1.addAnimation(expandAnim)
state1.addTransition(eventTransition1)
eventTransition2 = QEventTransition(self.expandToggle, QEvent.MouseButtonPress)
eventTransition2.setTargetState(state1)
eventTransition2.addAnimation(closeAnim)
state2.addTransition(eventTransition2)
# Add the states to the machine
machine.addState(state1)
machine.addState(state2)
machine.setInitialState(state1)
machine.start()
# Tree Widget
class CustomTree(QTreeWidget):
customWidget = []
def __init__(self, parent=None):
QTreeWidget.__init__(self, parent)
# Create top level items
itemCount = 3
for element in range(itemCount):
topLevelItem = QTreeWidgetItem()
self.addTopLevelItem(topLevelItem)
# Replace the top level items with the expandable custom widget:
# Get the Model Index to each item
modelIndex = self.indexFromItem(topLevelItem, 0)
# Create the ExpandableFrame widgets for all the top level items
self.customWidget.append(ExpandableFrame())
# Set the widget to each top level item based on its index
self.setIndexWidget(modelIndex, self.customWidget[element])
# Create more items and add them as children of the top level items
for x in range(itemCount):
child = QTreeWidgetItem()
child.setText(0,'Child')
topLevelItem.addChild(child)
This is how the example looks:
Minimal Code Example running:
One solution could be to emit the sizeHintChanged signal of the item delegate of your view every time the size of any of the widgets is changed. This tells the view that the position of the items should be updated. To achieve this you could override the resizeEvent of ExpandableFrame and emit a custom signal, e.g.
class ExpandableFrame(QFrame):
resized = pyqtSignal(QVariant)
def resizeEvent(self, event):
self.resized.emit(self.height())
super().resizeEvent(event)
In CustomTree, you can then connect ExpandableFrame.resized to the sizeHintChanged signal of the delegate of the custom tree by overriding setIndexWidget, e.g.
class CustomTree(QTreeWidget):
...
def setIndexWidget(self, index, widget):
super().setIndexWidget(index, widget)
if isinstance(widget, ExpandableFrame):
widget.resized.connect(lambda: self.itemDelegate().sizeHintChanged.emit(index))
Screen capture:
The disadvantage of this approach is that you would need to update the connections if the the widgets are moved or replaced.

Overlapping widgets in QLayout

I want to create a window where is Qt3DWindow in the back and some QPushButtons above it. However only Qt3DWindow animation is shown and QPushButtons are not seen. I would also like to have Qt3DWindow functional and QPushButtons as well (So I can click on the buttons or 3D animation behind). Buttons are only seen when I set Qt3DWindow transparency to lower value. Of course in that case buttons are only seen but not functional.
class MainWindow(QMainWindow):
def __init__(self, *args):
QMainWindow.__init__(self, *args)
self.window = Window() # Qt3DExtras.Qt3DWindow
self.container = self.createWindowContainer(self.window)
self.buttons = Buttons()
self.layout().addWidget(self.buttons.view) # QtWidgets.QGraphicsView
self.layout().addWidget(self.container)
QWidget::createWindowContainer() will handle the geometry of the window however it does change the fact that the managed window still overlays the window that contains the widget. So any child of that widget will not be visible as it will be obscured by the Qt3DWindow.
Only viable alternative is move the widgets you want to overlay into their own window and handles it's geometry yourself.
Or use a Scene3D inside a QDeclarativeWidget but that will affect performance.
As per the comment, QMainWindow uses its own layout type which is responsible for a large part (most) of its functionality -- dock widgets, toolbars etc.
Rather than just add widgets to that layout you need to create your own widget hierarchy and pass that to QMainWindow::setCentralWidget.
If you want the Buttons to lie in front of the container you can probably use a QGridLayout.
So, you could try something like (untested)...
class MainWindow(QMainWindow):
def __init__(self, *args):
QMainWindow.__init__(self, *args)
self.window = Window() # Qt3DExtras.Qt3DWindow
self.container = self.createWindowContainer(self.window)
self.buttons = Buttons()
central_widget = QWidget()
central_widget_layout = QGridLayout()
central_widget_layout.addWidget(self.container, 0, 0, 2, 1)
central_widget_layout.addWidget(self.buttons.view, 0, 0, 1, 1)
central_widget.setLayout(central_widget_layout)
setCentralWidget(central_widget)

Add a Grid in a matplotlib plot between different classes using a button

I want to connect a QPushButton in my QMainWindow with a class that i created using Matplotlib so i can show a grid when i push the button. This is a part of the code:
class Ventana(QMainWindow):
def __init__(self):
super(QMainWindow, self).__init__()
self.channel = ViewWidget()
#I add a toolbar and i put the button in here
self.toolbar2.addWidget(self.btn_showgrid)
self.btn_showgrid.setEnabled(True)
self.connect(self.btn_showgrid, SIGNAL("clicked()"), self.showGrid)
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.channel.axes.grid(True)
class ViewWidget(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.mainWidget = QWidget()
self.setCentralWidget(self.mainWidget)
layout = QVBoxLayout()
self.mainWidget.setLayout(layout)
self.figure_canvas = FigureCanvas(Figure())
layout.addWidget(self.figure_canvas, 10)
self.axes = self.figure_canvas.figure.add_subplot(111)
x = np.arange(0,5,0.1)
y = np.sin(x)
self.axes.plot(x,y)
My method called showGrid set the button to "disable" (is what i want to when the button is pushed) but it does not shows the grid. What am i doing wrong?
Hope you can help me. Thanks for your answers
------------------------- EDIT --------------------------------
I´ve made a few changes. I created the QPushButton and i added it to the toolbar of the plot.
# create a simple widget to extend the navigation toolbar
self.btn_showgrid = QPushButton("Show Grid")
self.btn_showgrid.setEnabled(True)
self.btn_hidegrid = QPushButton("Hide Grid")
self.btn_hidegrid.setEnabled(False)
layout = QVBoxLayout()
self.mainWidget.setLayout(layout)
self.figure_canvas = FigureCanvas(Figure())
layout.addWidget(self.figure_canvas, 10)
self.axes = self.figure_canvas.figure.add_subplot(111)
self.axes.grid(False)
x = np.arange(0,5,0.1)
y = np.sin(x)
self.axes.plot(x,y)
I also put a line: self.axes.grid(False) as you can see above. And at last i created this method:
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.btn_hidegrid.setEnabled(True)
self.axes.grid(True)
self.axes.draw()
The problem now is that when i push the button, it only hides like it should, but the grid does no show. But if create a new plot in the same `QMainWindow, it works!!!!
I think i need to refresh the plot at the moment i make self.axes.grid(True), but the draw() does not work. How can i accomplish this? I mean, refresh the plot?
You need to tell the canvas to re-draw. Drawing can be expensive so updating the state of the artists does not trigger a re-draw (the pyplot API does do that but you should not use that here). I think
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.channel.axes.grid(True)
self.channel.canvas.draw_idle()
will do the trick. The call to draw_idle tells Qt to, the next time it re-paints window to also trigger an Agg redraw for mpl, and to please schedule a repaint 'soon'.

A QWidget like QTextEdit that wraps its height automatically to its contents?

I am creating a form with some QTextEdit widgets.
The default height of the QTextEdit exceeds a single line of text and as the contents' height exceeds the QTextEdit's height, it creates a scroll-bar to scroll the content.
I would like to override this behaviour to create a QTextEdit that would rather wrap its height to its contents. This means that the default height would be one line and that on wrapping or entering a new line, the QTextEdit would increase its height automatically. Whenever the contents height exceeds the QTextEdit's height, the latter should not create a scroll bar but simply increase in height.
How can I go about doing this? Thanks.
This is almost exactly like a question I answer the other day about making a QTextEdit adjust its height in reponse to content changes: PySide Qt: Auto vertical growth for TextEdit Widget
I am answering instead of marking a duplicate as I suspect its possible you want a variation on this. Let me know if you want me to expand this answer:
The other question had multiple parts. Here is the excerpt of the growing height widget:
class Window(QtGui.QDialog):
def __init__(self):
super(Window, self).__init__()
self.resize(600,400)
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.setMargin(10)
self.scroll = QtGui.QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.mainLayout.addWidget(self.scroll)
scrollContents = QtGui.QWidget()
self.scroll.setWidget(scrollContents)
self.textLayout = QtGui.QVBoxLayout(scrollContents)
self.textLayout.setMargin(10)
for _ in xrange(5):
text = GrowingTextEdit()
text.setMinimumHeight(50)
self.textLayout.addWidget(text)
class GrowingTextEdit(QtGui.QTextEdit):
def __init__(self, *args, **kwargs):
super(GrowingTextEdit, self).__init__(*args, **kwargs)
self.document().contentsChanged.connect(self.sizeChange)
self.heightMin = 0
self.heightMax = 65000
def sizeChange(self):
docHeight = self.document().size().height()
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
the following code sets a QTextEdit widget to the height of the content:
# using QVBoxLayout in this example
grid = QVBoxLayout()
text_edit = QTextEdit('Some content. I make this a little bit longer as I want to see the effect on a widget with more than one line.')
# read-only
text_edit.setReadOnly(True)
# no scroll bars in this example
text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
text_edit.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
# you can set the width to a specific value
# text_edit.setFixedWidth(400)
# this is the trick, we nee to show the widget without making it visible.
# only then the document is created and the size calculated.
# Qt.WA_DontShowOnScreen = 103, PyQt does not have this mapping?!
text_edit.setAttribute(103)
text_edit.show()
# now that we have a document we can use it's size to set the QTextEdit's size
# also we add the margins
text_edit.setFixedHeight(text_edit.document().size().height() + text_edit.contentsMargins().top()*2)
# finally we add the QTextEdit to our layout
grid.addWidget(text_edit)
I hope this helps.

Categories