Related
I have a window which holds two plots, one is a 2D plot and the other is a selection of a cut along the y-axis of that plot. I would like to be able to select what cut I want by moving a horizontal bar up to a position and having the 1D plot update. I am having trouble relating the position from the scene where the line item is, to the axes on the plot. The part where I would be figuring this out is in the main window in the function defined plot_position. I am also open to other ideas of how to go about this. Here is a screen-shot for reference:
import sys
from PyQt5.Qt import Qt, QObject, QPen, QPointF
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsLineItem, QGraphicsView, \
QGraphicsScene, QWidget, QHBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import xarray as xr
import numpy as np
class Signals(QObject):
bttnReleased = pyqtSignal(float, float)
class HLineItem(QGraphicsLineItem):
def __init__(self, signals):
super(HLineItem, self).__init__()
self.signals = signals
self.setPen(QPen(Qt.red, 3))
self.setFlag(QGraphicsLineItem.ItemIsMovable)
self.setCursor(Qt.OpenHandCursor)
self.setAcceptHoverEvents(True)
def mouseMoveEvent(self, event):
orig_cursor_position = event.lastScenePos()
updated_cursor_position = event.scenePos()
orig_position = self.scenePos()
updated_cursor_y = updated_cursor_position.y() - \
orig_cursor_position.y() + orig_position.y()
self.setPos(QPointF(orig_position.x(), updated_cursor_y))
def mouseReleaseEvent(self, event):
x_pos = event.scenePos().x()
y_pos = event.scenePos().y()
self.signals.bttnReleased.emit(x_pos, y_pos)
class PlotCanvas(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
super(PlotCanvas, self).__init__(self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.data = xr.DataArray()
self.axes = None
def plot(self, data):
self.data = data
self.axes = self.fig.add_subplot(111)
self.data.plot(ax=self.axes)
self.axes.set_xlim(-.5, .5)
self.draw()
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.signals = Signals()
x = np.linspace(-1, 1, 51)
y = np.linspace(-1, 1, 51)
z = np.linspace(-1, 1, 51)
xyz = np.meshgrid(x, y, z, indexing='ij')
d = np.sin(np.pi * np.exp(-1 * (xyz[0] ** 2 + xyz[1] ** 2 + xyz[2] ** 2))) * np.cos(np.pi / 2 * xyz[1])
self.xar = xr.DataArray(d, coords={"slit": x, 'perp': y, "energy": z}, dims=["slit", "perp", "energy"])
self.cut = self.xar.sel({"perp": 0}, method='nearest')
self.edc = self.cut.sel({'slit': 0}, method='nearest')
self.canvas = PlotCanvas()
self.canvas_edc = PlotCanvas()
self.canvas.plot(self.cut)
self.canvas_edc.plot(self.edc)
self.view = QGraphicsView()
self.scene = QGraphicsScene()
self.line = HLineItem(self.signals)
self.line_pos = [0, 0]
self.layout1 = QHBoxLayout()
self.layout2 = QHBoxLayout()
self.connect_scene()
self.layout1.addWidget(self.view)
self.layout2.addWidget(self.canvas_edc)
self.central = QWidget()
self.main_layout = QHBoxLayout()
self.main_layout.addLayout(self.layout1)
self.main_layout.addLayout(self.layout2)
self.central.setLayout(self.main_layout)
self.setCentralWidget(self.central)
self.signals.bttnReleased.connect(self.plot_position)
def connect_scene(self):
s = self.canvas.figure.get_size_inches() * self.canvas.figure.dpi
self.view.setScene(self.scene)
self.scene.addWidget(self.canvas)
# self.scene.setSceneRect(0, 0, s[0], s[1])
# self.capture_scene_change()
self.line.setLine(0, 0, self.scene.sceneRect().width(), 0)
self.line.setPos(self.line_pos[0], self.line_pos[1])
self.scene.addItem(self.line)
def handle_plotting(self):
self.clearLayout(self.layout2)
self.refresh_edc()
def refresh_edc(self):
self.canvas_edc = PlotCanvas()
self.canvas_edc.plot(self.edc)
self.layout2.addWidget(self.canvas_edc)
def clearLayout(self, layout):
while layout.count():
child = layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
def plot_position(self, x, y):
self.line_pos = [x, y]
plot_bbox = self.canvas.axes.get_position()
# something here to relate the hline position in the scene to the axes positions/plot axes
sel_val = min(self.cut.slit, key=lambda f: abs(f - y)) # this should not be y, but rather
# the corresponding value on the y-axis
self.edc = self.cut.sel({"slit": 0}, method='nearest') # this should not be 0, but rather
# the sel_val
self.handle_plotting()
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.setAttribute(Qt.AA_EnableHighDpiScaling)
self.mainWindow = MainWindow()
self.mainWindow.setWindowTitle("arpys")
self.mainWindow.show()
def main():
app = App(sys.argv)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
This took me quite a while to figure out. The frustrating part was dealing with Matplotlib's positions of their objects and understanding what classes include what. For a while I was trying to use axes.get_position but this was returning a value for the height that was way too small. I am still not sure for this function what they define as y1 and y0 to give the height. Next I looked at axes.get_window_extent (had a similar problem) and axes.get_tightbbox. the tightbbox function returns the bounding box of the axes including their decorators (xlabel, title, etc) which was better but was actually giving me a height that was beyond the extent I wanted since it included these decorators. Finally, I found that what I really wanted with the length of the spine! In the end I used the spine.get_window_extent() function and this was exactly what I needed. I have attached the updated code.
It was helpful to look at this diagram of the inheritances to see what I was dealing with/looking for.
import sys
from PyQt5.Qt import Qt, QObject, QPen, QPointF
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsLineItem, QGraphicsView, \
QGraphicsScene, QWidget, QHBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import xarray as xr
import numpy as np
class Signals(QObject):
bttnReleased = pyqtSignal(float)
class HLineItem(QGraphicsLineItem):
def __init__(self, signals):
super(HLineItem, self).__init__()
self.signals = signals
self.setPen(QPen(Qt.red, 3))
self.setFlag(QGraphicsLineItem.ItemIsMovable)
self.setCursor(Qt.OpenHandCursor)
self.setAcceptHoverEvents(True)
def mouseMoveEvent(self, event):
orig_cursor_position = event.lastScenePos()
updated_cursor_position = event.scenePos()
orig_position = self.scenePos()
updated_cursor_y = updated_cursor_position.y() - \
orig_cursor_position.y() + orig_position.y()
self.setPos(QPointF(orig_position.x(), updated_cursor_y))
def mouseReleaseEvent(self, event):
y_pos = event.scenePos().y()
self.signals.bttnReleased.emit(y_pos)
class PlotCanvas(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
super(PlotCanvas, self).__init__(self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.data = xr.DataArray()
self.axes = None
def plot(self, data):
self.data = data
self.axes = self.fig.add_subplot(111)
self.data.plot(ax=self.axes)
self.fig.subplots_adjust(left=0.2)
self.fig.subplots_adjust(bottom=0.2)
self.axes.set_xlim(-.5, .5)
self.draw()
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.signals = Signals()
x = np.linspace(-1, 1, 51)
y = np.linspace(-1, 1, 51)
z = np.linspace(-1, 1, 51)
xyz = np.meshgrid(x, y, z, indexing='ij')
d = np.sin(np.pi * np.exp(-1 * (xyz[0] ** 2 + xyz[1] ** 2 + xyz[2] ** 2))) * np.cos(np.pi / 2 * xyz[1])
self.xar = xr.DataArray(d, coords={"slit": x, 'perp': y, "energy": z}, dims=["slit", "perp", "energy"])
self.cut = self.xar.sel({"perp": 0}, method='nearest')
self.edc = self.cut.sel({'slit': 0}, method='nearest')
self.canvas = PlotCanvas()
self.canvas_edc = PlotCanvas()
self.canvas.plot(self.cut)
self.canvas_edc.plot(self.edc)
self.view = QGraphicsView()
self.scene = QGraphicsScene()
self.line = HLineItem(self.signals)
self.line_pos = [0, 0]
self.layout1 = QHBoxLayout()
self.layout2 = QHBoxLayout()
self.connect_scene()
self.layout1.addWidget(self.view)
self.layout2.addWidget(self.canvas_edc)
self.central = QWidget()
self.main_layout = QHBoxLayout()
self.main_layout.addLayout(self.layout1)
self.main_layout.addLayout(self.layout2)
self.central.setLayout(self.main_layout)
self.setCentralWidget(self.central)
self.signals.bttnReleased.connect(self.plot_position)
def connect_scene(self):
s = self.canvas.figure.get_size_inches() * self.canvas.figure.dpi
self.view.setScene(self.scene)
self.scene.addWidget(self.canvas)
self.scene.setSceneRect(0, 0, s[0], s[1])
# self.capture_scene_change()
self.line.setLine(0, 0, self.scene.sceneRect().width(), 0)
self.line.setPos(self.line_pos[0], self.line_pos[1])
self.scene.addItem(self.line)
def handle_plotting(self):
self.clearLayout(self.layout2)
self.refresh_edc()
def refresh_edc(self):
self.canvas_edc = PlotCanvas()
self.canvas_edc.plot(self.edc)
self.layout2.addWidget(self.canvas_edc)
def clearLayout(self, layout):
while layout.count():
child = layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
def plot_position(self, y):
rel_pos = lambda x: abs(self.scene.sceneRect().height() - x)
bbox = self.canvas.axes.spines['left'].get_window_extent()
plot_bbox = [bbox.y0, bbox.y1]
if rel_pos(y) < plot_bbox[0]:
self.line.setPos(0, rel_pos(plot_bbox[0]))
elif rel_pos(y) > plot_bbox[1]:
self.line.setPos(0, rel_pos(plot_bbox[1]))
self.line_pos = self.line.pos().y()
size_range = len(self.cut.slit)
r = np.linspace(plot_bbox[0], plot_bbox[1], size_range).tolist()
corr = list(zip(r, self.cut.slit.values))
sel_val = min(r, key=lambda f: abs(f - rel_pos(self.line_pos)))
what_index = r.index(sel_val)
self.edc = self.cut.sel({"slit": corr[what_index][1]}, method='nearest')
self.handle_plotting()
class App(QApplication):
def __init__(self, sys_argv):
super(App, self).__init__(sys_argv)
self.setAttribute(Qt.AA_EnableHighDpiScaling)
self.mainWindow = MainWindow()
self.mainWindow.setWindowTitle("arpys")
self.mainWindow.show()
def main():
app = App(sys.argv)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I have an application where I draw 2 custom widgets and then draw a line between them. I want to add a mousePressEvent to the line.
What would be the best way to do this?
I suppose I could create a QWidget of x pixel thickness and y length and then fill in the whole widget with the colour I want the line to have. Then the QWidget has the mousePressEvent that I can override. This doesn't seem like the most elegant solution and feels more like a workaround. Is there a better way?
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPaintEvent, QPainter, QPen, QFont
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QLabel
class MyWidget(QWidget):
def __init__(self, name, parent):
super().__init__(parent)
self.setAutoFillBackground(True)
self.setFixedSize(300, 100)
p = self.palette()
p.setColor(self.backgroundRole(), Qt.white)
self.setPalette(p)
lbl_name = QLabel(name, self)
lbl_name.setFont(QFont('Arial', 16))
lbl_name.move((self.width() - lbl_name.width()) / 2, self.height()/2 - lbl_name.height()/2)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.main_widget = QWidget(self)
self.widget_1 = MyWidget("Widget 1", self)
self.widget_1.move(50, 50)
self.widget_2 = MyWidget("Widget 2", self)
self.widget_2.move(700, 600)
self.resize(1200, 800)
self.setCentralWidget(self.main_widget)
def paintEvent(self, a0: QPaintEvent) -> None:
super().paintEvent(a0)
painter = QPainter(self)
painter.setPen(QPen(Qt.red, 3, Qt.SolidLine))
widget_1_x = self.widget_1.pos().x() + self.widget_1.size().width()
widget_1_y = self.widget_1.pos().y() + self.widget_1.size().height() / 2
widget_2_x = self.widget_2.pos().x()
widget_2_y = self.widget_2.pos().y() + self.widget_2.size().height() / 2
halfway_x = widget_1_x + (widget_2_x - widget_1_x) / 2
# add mousePressEvents to these lines:
painter.drawLine(widget_1_x, widget_1_y, halfway_x, widget_1_y)
painter.drawLine(halfway_x, widget_1_y, halfway_x, widget_2_y)
painter.drawLine(halfway_x, widget_2_y, widget_2_x, widget_2_y)
if __name__ == "__main__":
app = QApplication(sys.argv)
myapp = MainWindow()
myapp.show()
sys.exit(app.exec_())
How the above code looks like when run
I have a custom Media Player, that can display images and videos with the help of PyQt. Media player is implemented by the following code in python:
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout,
QLabel, \
QSlider, QStyle, QSizePolicy, QFileDialog
import sys
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtGui import QIcon, QPalette
from PyQt5.QtCore import Qt, QUrl
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("PyQt5 Media Player")
self.setGeometry(350, 100, 700, 500)
self.setWindowIcon(QIcon('player.png'))
p =self.palette()
p.setColor(QPalette.Window, Qt.black)
self.setPalette(p)
self.init_ui()
self.show()
def init_ui(self):
#create media player object
self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface)
#create videowidget object
videowidget = QVideoWidget()
#create open button
openBtn = QPushButton('Open Video')
openBtn.clicked.connect(self.open_file)
#create button for playing
self.playBtn = QPushButton()
self.playBtn.setEnabled(False)
self.playBtn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
self.playBtn.clicked.connect(self.play_video)
#create slider
self.slider = QSlider(Qt.Horizontal)
self.slider.setRange(0,0)
self.slider.sliderMoved.connect(self.set_position)
#create label
self.label = QLabel()
self.label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
#create hbox layout
hboxLayout = QHBoxLayout()
hboxLayout.setContentsMargins(0,0,0,0)
#set widgets to the hbox layout
hboxLayout.addWidget(openBtn)
hboxLayout.addWidget(self.playBtn)
hboxLayout.addWidget(self.slider)
#create vbox layout
vboxLayout = QVBoxLayout()
vboxLayout.addWidget(videowidget)
vboxLayout.addLayout(hboxLayout)
vboxLayout.addWidget(self.label)
self.setLayout(vboxLayout)
self.mediaPlayer.setVideoOutput(videowidget)
#media player signals
self.mediaPlayer.stateChanged.connect(self.mediastate_changed)
self.mediaPlayer.positionChanged.connect(self.position_changed)
self.mediaPlayer.durationChanged.connect(self.duration_changed)
def open_file(self):
filename, _ = QFileDialog.getOpenFileName(self, "Open Video")
if filename != '':
self.mediaPlayer.setMedia(QMediaContent(QUrl.fromLocalFile(filename)))
self.playBtn.setEnabled(True)
def play_video(self):
if self.mediaPlayer.state() == QMediaPlayer.PlayingState:
self.mediaPlayer.pause()
else:
self.mediaPlayer.play()
def mediastate_changed(self, state):
if self.mediaPlayer.state() == QMediaPlayer.PlayingState:
self.playBtn.setIcon(
self.style().standardIcon(QStyle.SP_MediaPause)
)
else:
self.playBtn.setIcon(
self.style().standardIcon(QStyle.SP_MediaPlay)
)
def position_changed(self, position):
self.slider.setValue(position)
def duration_changed(self, duration):
self.slider.setRange(0, duration)
def set_position(self, position):
self.mediaPlayer.setPosition(position)
def handle_errors(self):
self.playBtn.setEnabled(False)
self.label.setText("Error: " + self.mediaPlayer.errorString())
app = QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
What I am trying to do is get the x and y coordinates of the edges of the video/image played each time and while it feels like it should be easy I really can't figure out how to do this. As displayed in the images every video/image may have different corner positions. The only thing I could think of was getting videowidgets dimensions but it wasn't right.
print(videowidget.height())
print(videowidget.width())
print(videowidget.x())
print(videowidget.y())
I'm not sure if this exactly answers your question but I found a sort of solution by comparing aspect ratios of the video and the widget:
class VideoClickWidget(QVideoWidget):
def __init__(self):
QVideoWidget.__init__(self)
def mouseReleaseEvent(self, event):
widget_width = self.frameGeometry().width()
widget_height = self.frameGeometry().height()
widget_ratio = widget_width / widget_height
video_width = self.mediaObject().metaData("Resolution").width()
video_height = self.mediaObject().metaData("Resolution").height()
video_ratio = video_width / video_height
x, y = event.pos().x(), event.pos().y()
# It's wider
if widget_ratio > video_ratio:
percentage = video_ratio / widget_ratio
# we know that video occupies $percentage$ of the widget
dead_zone = int(np.round(widget_width * ((1 - percentage) / 2)))
new_x = np.clip(x - dead_zone, 0, widget_width - 2 * dead_zone)
print(new_x, y)
else:
percentage = widget_ratio / video_ratio
dead_zone = int(np.round(widget_height * ((1 - percentage) / 2)))
new_y = np.clip(y - dead_zone, 0, widget_height - 2 * dead_zone)
print(x, new_y)
super(QVideoWidget, self).mouseReleaseEvent(event)
I've created a small app and I'm trying to make it so that when the main window is resized (and the GraphicsView and scene are too) that the whole scene (pixmap and rectangles) scale vertically to fit completely inside the GraphicsView. I don't want a vertical scrollbar and I don't want it to scale horizontally.
I can't figure out how to scale the scene properly. I use a GraphicsScene to contain a graph and a couple vertical rectangle "markers". When I can scale the graph to fit by redrawing the pixmap and then reattach it, the z-order is wrong AND the rectangle widgets are not scaled with it.
I need to keep track of the rectangle widgets, so I can't just keep deleting and re-adding them as there's meta data along with each one.
I know about fitInView (from here: Issue with fitInView of QGraphicsView when ItemIgnoresTransformations is on) that applies to the containing GraphicsView, but I don't understand why it needs a parameter. I just want the scene to fit in the GraphicsView (vertically but not horizontally) so why doesn't GraphicsView just scale everything in the scene to fit inside it's current size? What should the parameter look like to get the scene to fit vertically?
In the resizeEvent I can redraw the pixmap and re-add, but then it covers the rectangles as the z-order is messed up. Also, it doesn't stay centered vertically in the scene and I would need to copy over the meta data.
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
import PyQt5 as qt
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QGroupBox, QDialog, QVBoxLayout
from PyQt5.QtWidgets import QVBoxLayout, QGridLayout, QStackedWidget, QTabWidget
import numpy as np
class GraphicsScene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(GraphicsScene, self).__init__(parent)
def minimumSizeHint(self):
return QtCore.QSize(300, 200)
def dragMoveEvent(self, event):
print("dragMoveEvent", event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
#super(MainWindow).__init__()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
max_x, max_y = 2400, 700
max_x_view = 1200
self.max_x = max_x
self.max_y = max_y
self.first = True
self.setGeometry(200, 200, max_x_view, self.max_y)
self.gv = QtWidgets.QGraphicsView(self)
self.gv.setGeometry(0, 0, max_x_view, self.max_y)
self.gv2 = QtWidgets.QGraphicsView(self)
layout.addWidget(self.gv)
layout.addWidget(self.gv2)
scene = GraphicsScene()
self.scene = scene
self.gv.setScene(scene)
tab_widget = QTabWidget()
tab_widget.setTabPosition(QTabWidget.West)
widget = QWidget()
widget.setLayout(layout)
tab_widget.addTab(widget, "main")
self.setCentralWidget(tab_widget)
np.random.seed(777)
self.x_time = np.linspace(0, 12.56, 3000)
rand_data = np.random.uniform(0.0, 1.0, 3000)
self.data = .45*(np.sin(2*self.x_time) + rand_data) - .25*(np.sin(3*self.x_time))
self.first = True
pixmap_height = max_y//2 - 2*22 # 22 to take care of scrollbar height
pixmap = self.draw_graph()
pen = QtGui.QPen()
pen.setWidth(2)
pen.setColor(QtGui.QColor("red"))
self.gv1_pixmap = scene.addPixmap(pixmap)
rect = scene.sceneRect()
print("scene rect = {}".format(rect))
scene.setSceneRect(rect)
side, offset = 50, 200
for i in range(2):
r = QtCore.QRectF(QtCore.QPointF((i + 1)*offset + i * 2 * side, 2), QtCore.QSizeF(side, pixmap_height - 4))
rect_ref = scene.addRect(r, pen, QColor(255, 0, 0, 127))
rect_ref.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
all_items = scene.items()
print(all_items)
def draw_graph(self):
print("draw_graph: main Window size {}:".format(self.size()))
pixmap_height = self.height()//2 - 2*22 # 22 to take care of scrollbar height
x_final = self.x_time[-1]
data = self.data / np.max(np.abs(self.data))
data = [abs(int(k * pixmap_height)) for k in self.data]
x_pos = [int(self.x_time[i] * self.max_x / x_final) for i in range(len(data))]
pixmap = QtGui.QPixmap(self.max_x, pixmap_height)
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen()
pen.setWidth(2)
rect = pixmap.rect()
pen.setColor(QtGui.QColor("red"))
painter.drawRect(rect)
print("pixmap rect = {}".format(rect))
painter.fillRect(rect, QtGui.QColor('lightblue'))
pen.setWidth(2)
pen.setColor(QtGui.QColor("green"))
painter.setPen(pen)
for x, y in zip(x_pos, data):
painter.drawLine(x, pixmap_height, x, pixmap_height - y)
painter.end()
return pixmap
def resizeEvent(self, a0: QtGui.QResizeEvent):
#print("main Window resizeEvent")
print("main Window size {}:".format(a0.size()))
redraw = False
if redraw:
pixmap = self.draw_graph()
self.scene.removeItem(self.gv1_pixmap)
self.gv1_pixmap = self.scene.addPixmap(pixmap)
self.gv1_pixmap.moveBy(0, 30)
else:
#rect = QtCore.QRect(self.gv.startPos, self.gv.endPos)
#sceneRect = self.gv.mapToScene(rect).boundingRect()
#print 'Selected area: viewport coordinate:', rect,', scene coordinate:', sceneRect
#self.gv.fitInView(sceneRect)
pass
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
My solution will fit the height of the smallest rectangle that encapsulates all the items (sceneRect) to the viewport of the QGraphicsView. So set the height of the items a value not so small so that the image quality is not lost. I have also scaled the items using the QTransforms. In addition, the QGraphicsView coordinate system was inverted since by default the vertical axis is top-bottom and I have inverted it so that the painting is more consistent with the data.
I have refactored the OP code to make it more scalable, there is a GraphItem that takes the data (x, y) and the image dimensions.
Considering the above, the solution is:
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphItem(QtWidgets.QGraphicsPixmapItem):
def __init__(self, xdata, ydata, width, height, parent=None):
super(GraphItem, self).__init__(parent)
self._xdata = xdata
self._ydata = ydata
self._size = QtCore.QSize(width, height)
self.redraw()
def redraw(self):
x_final = self._xdata[-1]
pixmap = QtGui.QPixmap(self._size)
pixmap_height = pixmap.height()
pixmap.fill(QtGui.QColor("lightblue"))
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen(QtGui.QColor("green"))
pen.setWidth(2)
painter.setPen(pen)
for i, (x, y) in enumerate(
zip(self._xdata, self._ydata / np.max(np.abs(self._ydata)))
):
x_pos = int(x * self._size.width() / x_final)
y_pos = abs(int(y * pixmap_height))
painter.drawLine(x_pos, 0, x_pos, y_pos)
painter.end()
self.setPixmap(pixmap)
class HorizontalRectItem(QtWidgets.QGraphicsRectItem):
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.scene():
newPos = self.pos()
newPos.setX(value.x())
return newPos
return super(HorizontalRectItem, self).itemChange(change, value)
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self.scale(1, -1)
def resizeEvent(self, event):
h = self.mapToScene(self.viewport().rect()).boundingRect().height()
r = self.sceneRect()
r.setHeight(h)
self.setSceneRect(r)
height = self.viewport().height()
for item in self.items():
item_height = item.boundingRect().height()
tr = QtGui.QTransform()
tr.scale(1, height / item_height)
item.setTransform(tr)
super(GraphicsView, self).resizeEvent(event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
tab_widget = QtWidgets.QTabWidget(tabPosition=QtWidgets.QTabWidget.West)
self.setCentralWidget(tab_widget)
self.graphics_view_top = GraphicsView()
self.graphics_view_bottom = QtWidgets.QGraphicsView()
container = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(container)
lay.addWidget(self.graphics_view_top)
lay.addWidget(self.graphics_view_bottom)
tab_widget.addTab(container, "main")
self.resize(640, 480)
side, offset, height = 50, 200, 400
np.random.seed(777)
x_time = np.linspace(0, 12.56, 3000)
rand_data = np.random.uniform(0.0, 1.0, 3000)
data = 0.45 * (np.sin(2 * x_time) + rand_data) - 0.25 * (np.sin(3 * x_time))
graph_item = GraphItem(x_time, data, 3000, height)
self.graphics_view_top.scene().addItem(graph_item)
for i in range(2):
r = QtCore.QRectF(
QtCore.QPointF((i + 1) * offset + i * 2 * side, 2),
QtCore.QSizeF(side, height),
)
it = HorizontalRectItem(r)
it.setPen(QtGui.QPen(QtGui.QColor("red"), 2))
it.setBrush(QtGui.QColor(255, 0, 0, 127))
self.graphics_view_top.scene().addItem(it)
it.setFlags(
it.flags()
| QtWidgets.QGraphicsItem.ItemIsMovable
| QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main()
I've created aQMainWindow with menubar and 4 dockable widgets. First dockwidget contents multipletabs, second is Qpainter widget, third is Matlabplot and fourth is pdf report.
When I run the code shows up like this below.
I want to be like below.
I want to divide screen into four widget automatically whenever it runs at any screen, And I want to have tabs to resize to its content.
Or do you have any better idea of having such widget, you are welcome to come with it.
Update of code
Resize of Qdockwidget brings this post forward. It seems that Qt Qdockwidget resize has been an issue for long time ago. I find it very difficult to program my Qmainwindow with 4 Qdockwidget, which the dock would fit and resize according to its contents, with other words, child widget. and According to Qt documentation, Qdockwidget resizes and respect the size of child Widgets. to get straight to problem, my mainwindow has 4 qdockwidgets, I would like to have them resizable according to contents.
What I have tried and used so far.
I have used following size functions.
self.sizeHint, self.minimumSize(), self.maximumSize() and self.setFixedSize(self.sizeHint()).
I am able to fix the size of contents in first Qdockwidget by using following codes.
self.setFixedSize(self.sizeHint())
Above code is written in the child widgets Class widgets
But that is not enough in order to work it, despite following codes needed to run and effect.
self.first.setMinimumSize(self.first.sizeHint())
self.grid.setMinimumSize(self.grid.sizeHint())
self.third.setMinimumSize(self.third.sizeHint())
self.adjustSize()
self.first.setMinimumSize(self.first.minimumSizeHint())
self.grid.setMinimumSize(self.grid.minimumSizeHint())
self.third.setMinimumSize(self.third.minimumSizeHint())
Noting that still my dockwindow does not resize according to child widgets. Dockwidget expand and increase. One may ask, Qdockwidgets could arrange and control by resizeDocks(). This code line is used and tried, but still does not get the desired behaviour.
I have been looking around and could find some relevant questions.
C++ resize a docked Qt QDockWidget programmatically?
Forcing a QDockWidget to behave like a central widget when it comes to resizing
Create a QDockWidget that resizes to it's contents
Those questions do not solve my problem.
Visualization of my code launch
1- When code runs and display on screen.
2- Desired and wanted display by first run of software.
3- When user tabs between tabwidgets want to resize to its content as image below.
4- The code is given below.
import sys, os
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtWidgets import QMainWindow, QLabel, QGridLayout, QWidget,
QDesktopWidget, QApplication, QAction, QFileDialog,QColorDialog
from PyQt5.QtWidgets import QPushButton, QMessageBox, QDockWidget,
QTabWidget, QVBoxLayout, QGroupBox, QHBoxLayout, QFrame, QSplitter
from PyQt5.QtWidgets import QTableWidget, QRadioButton, QListWidget,
QCheckBox, QTextEdit, QDialog, QSizePolicy
from PyQt5.QtCore import QSize, Qt, QFileInfo, QFile
from PyQt5.QtGui import QIcon, QKeySequence, QPainter, QPalette, QPen,
QBrush, QTextCursor, QFont
import matplotlib.pyplot as plt
#plt.style.use('ggplot')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import seaborn as sns
iconroot = os.path.dirname(__file__)
class mywindow(QMainWindow):
def __init__(self):
super(mywindow, self).__init__()
self.setMinimumSize(QSize(1200,800))
self.setWindowTitle('My Graphic Window')
centralWidget = QWidget(self)
self.setCentralWidget(centralWidget)
gridLayout = QGridLayout(self)
centralWidget.setLayout(gridLayout)
qtRectangle = self.frameGeometry()
centerPoint = QDesktopWidget().availableGeometry().center()
qtRectangle.moveCenter(centerPoint)
self.move(qtRectangle.topLeft())
imageroot = QFileInfo(__file__).absolutePath()
# Greate new action
newaction = QAction(QIcon(imageroot +'/images/new.png'), '&New', self)
newaction.setShortcut('Ctrl+N')
newaction.setStatusTip('New document')
newaction.triggered.connect(self.newCall)
# Greate menu bar and add action
menubar = self.menuBar()
filemenu = menubar.addMenu('&Test')
filemenu.addAction(newaction)
# Get current screen geometry
self.Screen = QtWidgets.QDesktopWidget().screenGeometry()
print(self.Screen, self.Screen.height(), self.Screen.width())
# def createToolbar(self):
self.filetoolbar = self.addToolBar('File')
self.filetoolbar.addAction(newaction)
self.topleftdockwindow()
self.toprightdockwindow()
def newCall(self):
print('New')
# Greate dockable subwindow.
def topleftdockwindow(self):
topleftwindow = QDockWidget ('Info',self)
# Stick window to left or right
topleftwindow.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.addDockWidget(Qt.TopDockWidgetArea, topleftwindow)
topleftwindow.setWidget(createtabwidget())
topleftwindow.resize( topleftwindow.minimumSize() )
bottomleftwindow = QDockWidget("Matplot",self)
bottomleftwindow.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.addDockWidget(Qt.BottomDockWidgetArea, bottomleftwindow)
bottomleftwindow.setWidget(createplotwidget())
self.setDockNestingEnabled(True)
topleftwindow.resize( topleftwindow.minimumSize() )
self.splitDockWidget(topleftwindow, bottomleftwindow , Qt.Vertical)
#self.resizeDocks((topleftwindow, bottomleftwindow), (40,20),
#Qt.Horizontal)
# Greate topright dockwindow.
def toprightdockwindow(self):
toprightdock = QDockWidget ('Plot',self)
toprightdock = QDockWidget ('Plot',self)
toprightdock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.addDockWidget(Qt.TopDockWidgetArea, toprightdock)
#self.setDockOptions(self.AnimatedDocks | self.AllowNestedDocks)
toprightdock.setWidget(createpaintwidget())
toprightdock.setFloating( True )
bottomrightdock = QDockWidget("Technical report",self)
bottomrightdock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.addDockWidget(Qt.BottomDockWidgetArea, bottomrightdock)
bottomrightdock.setWidget(QtWidgets.QListWidget())
self.splitDockWidget(toprightdock, bottomrightdock, Qt.Vertical)
class createpaintwidget(QWidget):
def __init__(self):
super().__init__()
self.setBackgroundRole(QPalette.Base)
self.setAutoFillBackground(True)
self.sizeHint()
self.adjustSize()
def paintEvent(self, event):
self.pen = QPen()
self.brush = QBrush(Qt.gray,Qt.Dense7Pattern)
painter = QPainter(self)
painter.setPen(self.pen)
painter.setBrush(self.brush)
painter.drawRect(100,100,250,250)
painter.setBrush(QBrush())
painter.drawEllipse(400,100,200,200)
class createplotwidget(QWidget):
def __init__(self):
super().__init__()
self.initializewidget()
self.plot1()
self.setMaximumSize(self.sizeHint())
self.adjustSize()
def initializewidget(self):
self.setWindowTitle("Plotting M&N")
gridlayout = QGridLayout()
self.setLayout(gridlayout)
self.figure = plt.figure(figsize=(15,5))
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas,self)
gridlayout.addWidget(self.canvas,1,0,1,2)
gridlayout.addWidget(self.toolbar,0,0,1,2)
def plot1(self):
# sns.set()
ax = self.figure.add_subplot(111)
x = [i for i in range(100)]
y = [i**2 for i in x]
ax.plot(x,y, 'b.-')
ax.set_title('Quadratic Plot')
self.canvas.draw()
class createtextdocument(QWidget):
def __init__(self):
super().__init__()
self.textedit()
def textedit(self):
self.textedit = QTextEdit()
self.cursor = self.textedit.textCursor()
class createtabwidget(QDialog):
def __init__(self):
super().__init__()
# Greate tabs in dockable window
tab = QTabWidget()
scroll = QScrollArea()
ncroll = QScrollArea()
mcroll = QScrollArea()
self.first = firsttabgeometry()
self.grid = Grid()
self.third = thirdtabloads()
scroll.setWidget(self.first)
ncroll.setWidget(self.grid)
mcroll.setWidget(self.third)
scroll.setWidgetResizable(True)
self.first.setMinimumSize(self.first.sizeHint())
self.grid.setMinimumSize(self.grid.sizeHint())
self.third.setMinimumSize(self.third.sizeHint())
self.adjustSize()
self.first.setMinimumSize(self.first.minimumSizeHint())
self.grid.setMinimumSize(self.grid.minimumSizeHint())
self.third.setMinimumSize(self.third.minimumSizeHint())
# Adding multiple tabslides
tab.addTab(self.first,'One')
tab.addTab(self.grid,'Two')
tab.addTab(self.third,'Three')
tab.setFont(QFont("Georgia",10,QFont.Normal))
vboxlayout = QVBoxLayout()
vboxlayout.addWidget(tab)
self.setLayout(vboxlayout)
class firsttabgeometry(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(self.sizeHint())
iconroot = QFileInfo(__file__).absolutePath()
font = QFont("Georgia",10,QFont.Normal)
# Add widget and buttons to tabs
sectiontypegroupbox = QGroupBox('&One',self)
sectiontypegroupbox.setFont(QFont("Georgia",10,QFont.Normal))
tab1button = QPushButton('')
tab1button.setIcon(QIcon(iconroot +'/images/circularcolumn'))
tab1button.setIconSize(QSize(60,60))
tab1button.clicked.connect(self.One)
squarebutton = QPushButton('')
squarebutton.setIcon(QIcon(iconroot +'/images/squarecolumn'))
squarebutton.setIconSize(QSize(60,60))
squarebutton.clicked.connect(self.Two)
wallbutton = QPushButton("")
wallbutton.setIcon(QIcon(iconroot +'/images/wall'))
wallbutton.setIconSize(QSize(60,60))
wallbutton.clicked.connect(self.Three)
circularlabel = QLabel(" One",self)
circularlabel.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding)
circularlabel.setFont(font)
sclabel = QLabel(" Two",self)
sclabel.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding)
sclabel.setFont(font)
walllabel = QLabel(" Three",self)
walllabel.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding)
walllabel.setFont(font)
bottomgroupbox = QGroupBox("Group 2")
vboxlayout = QHBoxLayout()
vboxlayout.addStretch()
radiobutton2 = QRadioButton("Radio Button")
radiobutton3 = QRadioButton("Radio Button")
testbutton2 = QPushButton('Test Button 2')
vboxlayout.addWidget(radiobutton2)
vboxlayout.addWidget(radiobutton3)
vboxlayout.addWidget(testbutton2)
bottomgroupbox.setLayout(vboxlayout)
mainlayout = QGridLayout()
mainlayout.addWidget(tab1button,0,0)
mainlayout.addWidget(circularlabel,0,1)
mainlayout.addWidget(squarebutton,1,0)
mainlayout.addWidget(sclabel,1,1)
mainlayout.addWidget(wallbutton,2,0)
mainlayout.addWidget(walllabel,2,1)
mainlayout.setContentsMargins(200,50,50,50)
sectiontypegroupbox.setLayout(mainlayout)
gridlayout = QGridLayout()
gridlayout.addWidget(sectiontypegroupbox,1,0)
gridlayout.setContentsMargins(25,25,25,25)
self.setLayout(gridlayout)
def One(self):
print('One')
def Two(self):
print('Two')
def Three(self):
print('Three')
class FooWidget(QtWidgets.QWidget):
def __init__(self, path_icon, text, checked=False, parent=None):
super(FooWidget, self).__init__(parent)
lay = QtWidgets.QVBoxLayout(self)
pixmap = QtGui.QPixmap(os.path.join(iconroot, path_icon))
pixmap_label = QtWidgets.QLabel()
pixmap_label.resize(150, 150)
pixmap_label.setPixmap(pixmap.scaled(pixmap_label.size(), QtCore.Qt.KeepAspectRatio))
text_label = QtWidgets.QLabel(text)
checkbox = QtWidgets.QCheckBox(checked=checked)
lay.addWidget(pixmap_label)
lay.addWidget(text_label)
lay.addWidget(checkbox)
class Grid(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Grid, self).__init__(parent)
self.setFixedSize(self.sizeHint())
font = QFont("Georgia",8,QFont.Normal)
lay = QtWidgets.QHBoxLayout(self)
icons = ["images/fixed-fixed.png",
"images/pinned-pinned.png",
"images/fixed-free.png",
"images/fixed-pinned.png"]
texts = ["Ley = 1.0 L\nLec = 1.0 L",
"Ley = 0.699 L\nLec = 0.699 L",
"Ley = 2.0 L\nLec = 2.0 L",
"Ley = 0.5 L\nLec = 0.5 L"]
for path_icon, text in zip(icons, texts):
w = FooWidget(os.path.join(iconroot, path_icon), text)
lay.addWidget(w)
class thirdtabloads(QtWidgets.QWidget):
def __init__(self, parent=None):
super(thirdtabloads, self).__init__(parent)
self.adjustSize()
table = loadtable()
add_button = QtWidgets.QPushButton("Add")
add_button.clicked.connect(table._addrow)
delete_button = QtWidgets.QPushButton("Delete")
delete_button.clicked.connect(table._removerow)
copy_button = QtWidgets.QPushButton("Copy")
copy_button.clicked.connect(table._copyrow)
button_layout = QtWidgets.QVBoxLayout()
button_layout.addWidget(add_button, alignment=QtCore.Qt.AlignBottom)
button_layout.addWidget(delete_button, alignment=QtCore.Qt.AlignTop)
button_layout.addWidget(copy_button, alignment=QtCore.Qt.AlignTop )
tablehbox = QtWidgets.QHBoxLayout()
tablehbox.setContentsMargins(10,10,10,10)
tablehbox.addWidget(table)
grid = QtWidgets.QGridLayout(self)
grid.addLayout(button_layout, 0, 1)
grid.addLayout(tablehbox, 0, 0)
def copy_widget(w):
if isinstance(w, QtWidgets.QWidget):
new_w = type(w)()
if isinstance(w, QtWidgets.QComboBox):
vals = [w.itemText(ix) for ix in range(w.count())]
new_w.addItems(vals)
return new_w
class loadtable(QtWidgets.QTableWidget):
def __init__(self, parent=None):
super(loadtable, self).__init__(1, 5, parent)
self.setFont(QtGui.QFont("Helvetica", 10, QtGui.QFont.Normal, italic=False))
headertitle = ("Load Name","N [kN]","My [kNm]","Mz [kNm]","Load Type")
self.setHorizontalHeaderLabels(headertitle)
self.verticalHeader().hide()
self.horizontalHeader().setHighlightSections(False)
self.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.setColumnWidth(0, 130)
combox_lay = QtWidgets.QComboBox(self)
combox_lay.addItems(["ULS","SLS"])
self.setCellWidget(0, 4, combox_lay)
self.cellChanged.connect(self._cellclicked)
#QtCore.pyqtSlot(int, int)
def _cellclicked(self, r, c):
it = self.item(r, c)
it.setTextAlignment(QtCore.Qt.AlignCenter)
#QtCore.pyqtSlot()
def _addrow(self):
rowcount = self.rowCount()
self.insertRow(rowcount)
combox_add = QtWidgets.QComboBox(self)
combox_add.addItems(["ULS","SLS"])
self.setCellWidget(rowcount, 4, combox_add)
#QtCore.pyqtSlot()
def _removerow(self):
if self.rowCount() > 0:
self.removeRow(self.rowCount()-1)
#QtCore.pyqtSlot()
def _copyrow(self):
r = self.currentRow()
if 0 <= r < self.rowCount():
cells = {"items": [], "widgets": []}
for i in range(self.columnCount()):
it = self.item(r, i)
if it:
cells["items"].append((i, it.clone()))
w = self.cellWidget(r, i)
if w:
cells["widgets"].append((i, copy_widget(w)))
self.copy(cells, r+1)
def copy(self, cells, r):
self.insertRow(r)
for i, it in cells["items"]:
self.setItem(r, i, it)
for i, w in cells["widgets"]:
self.setCellWidget(r, i, w)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle("Fusion")
mainWin = mywindow()
mainWin.show()
mainWin.showMaximized()
sys.exit(app.exec_())
I would appreciate much any help on this.
If floating windows aren't essential to your tool then you can try dropping QDockWidget and using a series of QSplitter instead. This way you can have your nice box layout while having tabs to resize horizontally and vertically, and still resizing properly when the tool as a whole resizes.
My example is in PySide2, but you'll probably need to do very minor tweaks to PyQt5 (probably just the import names):
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
class SubWindow(QtWidgets.QWidget):
def __init__(self, label, parent=None):
super(SubWindow, self).__init__(parent)
self.label = QtWidgets.QLabel(label, parent=self)
self.label.setAlignment(QtCore.Qt.AlignCenter)
self.label.setStyleSheet("QLabel {font-size:40px;}")
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addWidget(self.label)
self.setLayout(self.main_layout)
class MainWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.sub_win_1 = SubWindow("1", parent=self)
self.sub_win_2 = SubWindow("2", parent=self)
self.sub_win_3 = SubWindow("3", parent=self)
self.sub_win_4 = SubWindow("4", parent=self)
self.sub_splitter_1 = QtWidgets.QSplitter(QtCore.Qt.Horizontal, parent=self)
self.sub_splitter_1.addWidget(self.sub_win_1)
self.sub_splitter_1.addWidget(self.sub_win_2)
self.sub_splitter_2 = QtWidgets.QSplitter(QtCore.Qt.Horizontal, parent=self)
self.sub_splitter_2.addWidget(self.sub_win_3)
self.sub_splitter_2.addWidget(self.sub_win_4)
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical, parent=self)
self.splitter.addWidget(self.sub_splitter_1)
self.splitter.addWidget(self.sub_splitter_2)
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addWidget(self.splitter)
self.setLayout(self.main_layout)
self.setWindowTitle("Layout example")
self.resize(500, 500)
inst = MainWindow()
inst.show()
This gives you something like this:
Right now the top/bottom horizontal splitters function separately, but you can easily tie them together with an event.
Hope that helps!