How do I auto fit a Matplotlib figure inside a PySide QFrame? - python-3.x

I'm creating a simple PySide application that also uses MatPlotLib. However, when I add the figure into a QFrame, the figure doesn't automatically fit to the frame:
My graph is created using the following code:
class GraphView(gui.QWidget):
def __init__(self, name, title, graphTitle, parent = None):
super(GraphView, self).__init__(parent)
self.name = name
self.graphTitle = graphTitle
self.dpi = 100
self.fig = Figure((5.0, 3.0), dpi = self.dpi, facecolor = (1,1,1), edgecolor = (0,0,0))
self.axes = self.fig.add_subplot(111)
self.canvas = FigureCanvas(self.fig)
self.Title = gui.QLabel(self)
self.Title.setText(title)
self.layout = gui.QVBoxLayout()
self.layout.addStretch(1)
self.layout.addWidget(self.Title)
self.layout.addWidget(self.canvas)
self.setLayout(self.layout)
def UpdateGraph(self, data, title = None):
self.axes.clear()
self.axes.plot(data)
if title != None:
self.axes.set_title(title)
self.canvas.draw()
And it's added to the main Widget like so:
# Create individual Widget/Frame (fftFrame)
fftFrame = gui.QFrame(self)
fftFrame.setFrameShape(gui.QFrame.StyledPanel)
self.FFTGraph = GraphView('fftFrame', 'FFT Transform:', 'FFT Transform of Signal', fftFrame)

Here's a working code sample that shows you how to get it working. I first thought it was because of the stretch you added to the layout, which will use up the additional space around the other widgets. But when I removed it, it still wouldn't resize. The 'easy' solution is to add a resizeEvent, which lets you define the size of your GraphView widget. In this case I just set its geometry to be that of the QFrame, though you might want to add some padding and make sure you set a sensible minimum size for the QFrame.
from PySide import QtGui
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import sys
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.fft_frame = FftFrame(self)
self.layout = QtGui.QVBoxLayout()
self.layout.addWidget(self.fft_frame)
self.setLayout(self.layout)
self.setCentralWidget(self.fft_frame)
class FftFrame(QtGui.QFrame):
def __init__(self, parent=None):
super(FftFrame, self).__init__(parent)
self.setFrameShape(QtGui.QFrame.StyledPanel)
self.parent = parent
self.graph_view = GraphView('fftFrame', 'FFT Transform:', 'FFT Transform of Signal', self)
def resizeEvent(self, event):
self.graph_view.setGeometry(self.rect())
class GraphView(QtGui.QWidget):
def __init__(self, name, title, graph_title, parent = None):
super(GraphView, self).__init__(parent)
self.name = name
self.graph_title = graph_title
self.dpi = 100
self.fig = Figure((5.0, 3.0), dpi = self.dpi, facecolor = (1,1,1), edgecolor = (0,0,0))
self.axes = self.fig.add_subplot(111)
self.canvas = FigureCanvas(self.fig)
self.canvas.setParent(self)
self.Title = QtGui.QLabel(self)
self.Title.setText(title)
self.layout = QtGui.QVBoxLayout()
self.layout.addWidget(self.Title)
self.layout.addWidget(self.canvas)
self.layout.setStretchFactor(self.canvas, 1)
self.setLayout(self.layout)
self.canvas.show()
def update_graph(self, data, title = None):
self.axes.clear()
self.axes.plot(data)
if title != None:
self.axes.set_title(title)
self.canvas.draw()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

Related

How to make a layout with only aboslute positioning in pyqt

I want to make a layout where the user is able to move the widget inside freely with no constraints of rows or column. So I'm using absolute positioning. I'm able to make it happen in a window with no layout but I don't know what to use as a layout , problem is in the commented line in the code.
from PyQt5.QtWidgets import QWidget, QLabel, QApplication,QVBoxLayout,QHBoxLayout
from PyQt5.QtCore import Qt, QMimeData
from PyQt5.QtGui import QDrag
class DragLabel(QLabel):
def mouseMoveEvent(self, e):
if e.buttons() == Qt.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
drag.exec_(Qt.MoveAction)
class Window(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.label = DragLabel('first', self)
self.label2 = DragLabel('second', self)
self.label3 = DragLabel('third', self)
self.label.move(50, 50)
self.label2.move(100, 100)
self.label3.move(150, 150)
# self.main_frame=QHBoxLayout()
# self.classic_frame=QVBoxLayout()
#what should I use here to make a frame working with absolute position only
# self.absolute_frame = ???
# self.absolute_frame.addWidget(self.label)
# self.absolute_frame.addWidget(self.label2)
# self.absolute_frame.addWidget(self.label3)
# self.main_frame.addLayout(self.classic_frame)
# self.main_frame.addLayout(self.absolute_frame)
# self.setLayout(self.main_frame)
def dragEnterEvent(self, e):
widget = e.source()
if isinstance(widget,DragLabel):
e.accept()
def dropEvent(self, e):
pos = e.pos()
widget_dep = e.source()
if isinstance(widget_dep,DragLabel):
widget_dep_name = widget_dep.text()
print(widget_dep_name)
widget_dep.move(pos.x(),pos.y())
if __name__ == '__main__':
app = QApplication([])
w = Window()
w.show()
app.exec_()

How to add scrollable regions with Drag & drop widgets with PyQt?

Please add a scrollable region to this code so that when I run the code, I can scroll down through the window and see the last items in the window and replace them.
As this is a drag and drop code I could not add scroll to it.
Moreover, during the drag and drop process, the scroll should work and does not interrupt the drag and drop process.
from PyQt5.QtWidgets import QApplication, QScrollArea, QHBoxLayout, QWidget, QLabel, QMainWindow, QVBoxLayout
from PyQt5.QtCore import Qt, QMimeData, pyqtSignal
from PyQt5.QtGui import QDrag, QPixmap
class DragItem(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setStyleSheet("border: 1px solid black;")
# Store data separately from display label, but use label for default.
self.data = self.text()
def set_data(self, data):
self.data = data
def mouseMoveEvent(self, e):
if e.buttons() == Qt.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec_(Qt.MoveAction)
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = pyqtSignal(list)
def __init__(self, *args, orientation=Qt.Orientation.Horizontal, **kwargs):
super().__init__()
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.pos()
widget = e.source()
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = pos.y() < w.y() + w.size().height() // 2
else:
# Drag drop horizontally.
drop_here = pos.x() < w.x() + w.size().width() // 2
if drop_here:
# We didn't drag past this widget.
# insert to the left of it.
self.blayout.insertWidget(n-1, widget)
self.orderChanged.emit(self.get_item_data())
break
e.accept()
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
data.append(w.data)
return data
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
self.scroll = QScrollArea() ###########
self.blayout = QHBoxLayout()
self.widget = QWidget()
for n, l in enumerate(['Art', 'Boo', 'EOA', 'Hel', \
'Hyg', 'Lei', 'Lei','Lei','Lei','Lei','Lei','Lei','Lei','Lei','Lei','Lei','Lei', 'Med',\
'Nut','Nut','Nut','Rel','Rel','Rel','Sle','SLN','Spo','Spo','Spo','Spo','Spo','Thi','Thi',\
'Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor','Wor']):
item = DragItem(l)
item.set_data(n) # Store the data.
self.drag.add_item(item)
def myFun():
print('hi', self.drag.get_item_data())
# Print out the changed order.
# self.drag.orderChanged.connect(print)
# add by me
self.drag.orderChanged.connect(myFun)
#Scroll Area Properties
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll.setWidgetResizable(True)
self.scroll.setWidget(self.widget)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
# self.setCentralWidget(self.scroll)
app = QApplication([])
w = MainWindow()
w.show()
app.exec_()

using pypubsub & wxpython to transfer data between windows getting TypeError:

I am trying to pass data between two wxpython windows using PyPubSub. Below I have pared the code down the the basics. What I am getting when I fill in the data in the textcntrl on the main window and hit the button to pass it to the second window (CoordFrame) I am getting the following error-
TypeError: sendMessage() takes 2 positional arguments but 3 were given
Every example I can find shows passing two parameters in the pub.sendMessage; which is what I think I'm doing. What is the 3rd parameter being passed, where does it come form and how do I stop it? I have tried various combinations of arguments, and datatypes including binary.
'''
from pubsub import pub
import wx
class MainFrame (wx.Frame):
def __init__(self, parent, title):
super(MainFrame, self).__init__(parent, title = title, size = (200,200))
self.panel = MainPanel(self)
class CoordFrame (wx.Frame):
def __init__(self, parent, title):
super(CoordFrame, self).__init__(parent, title = title, size = (200,200))
self.panel = CoordPanel(self)
class MainPanel(wx.Panel):
def __init__(self, parent):
super(MainPanel, self).__init__(parent)
vsizer = wx.BoxSizer(wx.VERTICAL)
self.tbxNewMap = wx.TextCtrl(self, id=1001, pos=(20,20), size = (50,20) ,style = wx.TE_CENTER|wx.TE_NOHIDESEL|wx.TE_PROCESS_ENTER)
vsizer.Add(self.tbxNewMap)
self.btnEnterNewMap = wx.Button(self, id=1002, label = "New Data", pos = (20,80), size = (80,40))
vsizer.Add(self.btnEnterNewMap,0,wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.onButtonNewMap, id=1002)
def onButtonNewMap(self,event):
temp = self.tbxNewMap.GetValue()
pub.sendMessage("coord_listener", temp)
coordframe = CoordFrame(None,"Entry")
coordframe.Show()
class CoordPanel(wx.Panel):
def __init__(self, parent):
super(CoordPanel, self).__init__(parent)
vsizer = wx.BoxSizer(wx.VERTICAL)
pub.subscribe(self.coord_listener, "coord_listener")
self.tbxNewMapNumber = wx.TextCtrl(self, id=1000, pos=(20,20), size = (50,20), style = wx.TE_CENTER|wx.TE_NOHIDESEL|wx.TE_PROCESS_ENTER)
vsizer.Add(self.tbxNewMapNumber)
def coord_listener(self, message):
newmapnum = message
self.tbxNewMapNumber.SetValue(newmapnum)
self.tbxNewMapNumber.Refresh()
class GMDash(wx.App):
def OnInit(self):
self.mainframe = MainFrame(parent = None, title = "Dashboard")
self.mainframe.Show()
return True
app = GMDash()
app.MainLoop()
'''
Use a named argument in the call to sendmessage and you'll need to register the listener before sending the message, not after, as you are currently doing.
See below:
from pubsub import pub
import wx
class MainFrame (wx.Frame):
def __init__(self, parent, title):
super(MainFrame, self).__init__(parent, title = title, size = (200,200))
self.panel = MainPanel(self)
class CoordFrame (wx.Frame):
def __init__(self, parent, title):
super(CoordFrame, self).__init__(parent, title = title, size = (200,200))
self.panel = CoordPanel(self)
class MainPanel(wx.Panel):
def __init__(self, parent):
super(MainPanel, self).__init__(parent)
vsizer = wx.BoxSizer(wx.VERTICAL)
self.tbxNewMap = wx.TextCtrl(self, id=1001, pos=(20,20), size = (50,20) ,style = wx.TE_CENTER|wx.TE_NOHIDESEL|wx.TE_PROCESS_ENTER)
vsizer.Add(self.tbxNewMap)
self.btnEnterNewMap = wx.Button(self, id=1002, label = "New Data", pos = (20,80), size = (80,40))
vsizer.Add(self.btnEnterNewMap,0,wx.EXPAND)
self.Bind(wx.EVT_BUTTON, self.onButtonNewMap, id=1002)
#Register the subscrption *before* sending the message
self.coordframe = CoordFrame(None,"Entry")
def onButtonNewMap(self,event):
temp = self.tbxNewMap.GetValue()
pub.sendMessage("coord_listener", message=temp)
#coordframe = CoordFrame(None,"Entry")
self.coordframe.Show()
class CoordPanel(wx.Panel):
def __init__(self, parent):
super(CoordPanel, self).__init__(parent)
vsizer = wx.BoxSizer(wx.VERTICAL)
pub.subscribe(self.coord_listener, "coord_listener")
self.tbxNewMapNumber = wx.TextCtrl(self, id=1000, pos=(20,20), size = (50,20), style = wx.TE_CENTER|wx.TE_NOHIDESEL|wx.TE_PROCESS_ENTER)
vsizer.Add(self.tbxNewMapNumber)
def coord_listener(self, message):
print(message)
newmapnum = message
self.tbxNewMapNumber.SetValue(newmapnum)
self.tbxNewMapNumber.Refresh()
class GMDash(wx.App):
def OnInit(self):
self.mainframe = MainFrame(parent = None, title = "Dashboard")
self.mainframe.Show()
return True
app = GMDash()
app.MainLoop()

PyQt: Is it possible to drag/drop QWidgets in a QGridLayout to rearrange them?

I am looking for a way to create a grid of graphs that can be dragged/dropped to rearrange the order. My first try was using QDockWidgets as they allow for drag/drop, however they were limited in a lot of other ways. Would it be possible to implement this function in a QGridLayout?
For now I have a QGridLayout with 3x3 matplotlib widgets.
Here is an example of the desired layout outcome.
Sample code:
import sys
from PyQt5 import QtWidgets
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
import random
from PyQt5.QtWidgets import QGridLayout, QVBoxLayout, QHBoxLayout, QScrollArea, QWidget, QDialog, QApplication, QFrame
class IndicSelectWindow(QDialog):
def __init__(self, parent=None):
super(IndicSelectWindow, self).__init__(parent=parent)
self.resize(1000, 800)
self.layout = QtWidgets.QHBoxLayout(self)
self.scrollArea = QScrollArea(self)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.gridLayout = QGridLayout(self.scrollAreaWidgetContents)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.layout.addWidget(self.scrollArea)
for i in range(3):
for j in range(3):
self.Frame = QFrame(self)
self.Frame.setStyleSheet("background-color: white;")
self.Frame.setFrameStyle(QFrame.Panel | QFrame.Raised)
self.Frame.setLineWidth(2)
self.layout = QHBoxLayout(self.Frame)
self.figure = Figure() # a figure to plot on
self.canvas = FigureCanvas(self.figure)
self.ax = self.figure.add_subplot(111) # create an axis
data = [random.random() for i in range(10)]
self.ax.plot(data, '*-') # plot data
self.canvas.draw() # refresh canvas
self.layout.addWidget(self.canvas)
Box = QVBoxLayout()
Box.addWidget(self.Frame)
self.gridLayout.addLayout(Box, i, j)
self.gridLayout.setColumnStretch(i % 3, 1)
self.gridLayout.setRowStretch(j, 1)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = IndicSelectWindow()
w.show()
sys.exit(app.exec_())
Here is an implementation that will swap the positions of the items involved in a drag/drop. The 3 main steps are:
(1) Reimplement mousePressEvent to get the index of the LayoutItem based on mouse coordinates.
(2) Reimplement mouseMoveEvent to set up a QDrag of the FigureCanvas.
(3) Reimplement dropEvent to swap the target items in the layout.
Since the matplotlib widgets absorb mouse events you also need to reimplement eventFilter to detect them.
import sys, random
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
class IndicSelectWindow(QDialog):
def __init__(self, parent=None):
super(IndicSelectWindow, self).__init__(parent=parent)
self.resize(1000, 800)
self.target = None
self.setAcceptDrops(True)
self.layout = QHBoxLayout(self)
self.scrollArea = QScrollArea(self)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.gridLayout = QGridLayout(self.scrollAreaWidgetContents)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.layout.addWidget(self.scrollArea)
for i in range(3):
for j in range(3):
self.Frame = QFrame(self)
self.Frame.setStyleSheet("background-color: white;")
self.Frame.setFrameStyle(QFrame.Panel | QFrame.Raised)
self.Frame.setLineWidth(2)
self.layout = QHBoxLayout(self.Frame)
self.figure = Figure() # a figure to plot on
self.canvas = FigureCanvas(self.figure)
self.ax = self.figure.add_subplot(111) # create an axis
data = [random.random() for i in range(10)]
self.ax.plot(data, '*-') # plot data
self.canvas.draw() # refresh canvas
self.canvas.installEventFilter(self)
self.layout.addWidget(self.canvas)
Box = QVBoxLayout()
Box.addWidget(self.Frame)
self.gridLayout.addLayout(Box, i, j)
self.gridLayout.setColumnStretch(i % 3, 1)
self.gridLayout.setRowStretch(j, 1)
def eventFilter(self, watched, event):
if event.type() == QEvent.MouseButtonPress:
self.mousePressEvent(event)
elif event.type() == QEvent.MouseMove:
self.mouseMoveEvent(event)
elif event.type() == QEvent.MouseButtonRelease:
self.mouseReleaseEvent(event)
return super().eventFilter(watched, event)
def get_index(self, pos):
for i in range(self.gridLayout.count()):
if self.gridLayout.itemAt(i).geometry().contains(pos) and i != self.target:
return i
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.target = self.get_index(event.windowPos().toPoint())
else:
self.target = None
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton and self.target is not None:
drag = QDrag(self.gridLayout.itemAt(self.target))
pix = self.gridLayout.itemAt(self.target).itemAt(0).widget().grab()
mimedata = QMimeData()
mimedata.setImageData(pix)
drag.setMimeData(mimedata)
drag.setPixmap(pix)
drag.setHotSpot(event.pos())
drag.exec_()
def mouseReleaseEvent(self, event):
self.target = None
def dragEnterEvent(self, event):
if event.mimeData().hasImage():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
if not event.source().geometry().contains(event.pos()):
source = self.get_index(event.pos())
if source is None:
return
i, j = max(self.target, source), min(self.target, source)
p1, p2 = self.gridLayout.getItemPosition(i), self.gridLayout.getItemPosition(j)
self.gridLayout.addItem(self.gridLayout.takeAt(i), *p2)
self.gridLayout.addItem(self.gridLayout.takeAt(j), *p1)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = IndicSelectWindow()
w.show()
sys.exit(app.exec_())

Using dynamically added widgets in PyQt/Pyside

I have modified the answer given here as written below. The code is basically creating pushbuttons with a counter as pushButton_0, pushButton_1..
Here, I know that when I press to self.addButton I am creating widgets named like self.pushButton_0, self.pushButton_1 etc. So, my question is, how I'm supposed to use this pushbuttons? Because when I'm trying to do something like self.pushButton_0.clicked.connect(self.x), it' s telling me that "there is no attribute named 'pushButton_0'".
Thanks!
from PyQt4 import QtGui, QtCore
import sys
class Main(QtGui.QMainWindow):
def __init__(self, parent = None):
super(Main, self).__init__()
self.GUI()
def GUI(self):
self.count = 0
# main button
self.addButton = QtGui.QPushButton('button to add other widgets')
self.addButton.clicked.connect(self.addWidget)
# scroll area widget contents - layout
self.scrollLayout = QtGui.QFormLayout()
# scroll area widget contents
self.scrollWidget = QtGui.QWidget()
self.scrollWidget.setLayout(self.scrollLayout)
# scroll area
self.scrollArea = QtGui.QScrollArea()
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setWidget(self.scrollWidget)
# main layout
self.mainLayout = QtGui.QVBoxLayout()
# add all main to the main vLayout
self.mainLayout.addWidget(self.addButton)
self.mainLayout.addWidget(self.scrollArea)
# central widget
self.centralWidget = QtGui.QWidget()
self.centralWidget.setLayout(self.mainLayout)
# set central widget
self.setCentralWidget(self.centralWidget)
def addWidget(self):
self.scrollLayout.addRow(Test(self))
self.count = self.count + 1
print(self.count)
class Test(QtGui.QWidget):
def __init__( self, main):
super(Test, self).__init__()
self.Main = main
self.setup()
def setup(self):
print(self.Main.count)
name = "pushButton_"+str(self.Main.count)
print(name)
self.name = QtGui.QPushButton('I am in Test widget '+str(self.Main.count))
layout = QtGui.QHBoxLayout()
layout.addWidget(self.name)
self.setLayout(layout)
app = QtGui.QApplication(sys.argv)
myWidget = Main()
myWidget.show()
app.exec_()
After hours, I found the problem!
You have to declare the signal while creating the pushbutton!
To fix this, I rewrote the setup function as below;
def setup(self):
print(self.Main.count)
name = "pushButton_"+str(self.Main.count)
print(name)
self.name = QtGui.QPushButton('I am in Test widget '+str(self.Main.count))
self.name.clicked.connect(self.x) # self.x is any function
layout = QtGui.QHBoxLayout()
layout.addWidget(self.name)
self.setLayout(layout)
So know, you will run function x whenever you push the new created pushbuttons!

Resources