I'm trying to create a video player similar to the looks of the default GUI for mpv. I'm using a QGraphicsVideoItem inside a QGraphicsView along with a custom ControlBar widget as the OSC.
I want the OSC to be 100px high and video.width()px wide, and always flush with the bottom edge of the QGraphicsView widget. I can't seem to do either of those requirements.
MRE:
from PySide6 import QtWidgets as qtw
from PySide6 import QtGui as qtg
from PySide6 import QtCore as qtc
from PySide6 import QtMultimedia as qtm
from PySide6 import QtMultimediaWidgets as qtmw
class ControlBar(qtw.QWidget):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.setStyleSheet("background: red")
class View(qtw.QGraphicsView):
def __init__(self) -> None:
super().__init__()
self.setMouseTracking(True)
self.setRenderHints(qtg.QPainter.RenderHint.SmoothPixmapTransform | qtg.QPainter.RenderHint.Antialiasing)
self.setViewportMargins(-2, -2, -2, -2) # `QGraphicsView` has hard coded margins.
self.setFrameStyle(qtw.QFrame.Shape.NoFrame)
self._scene = qtw.QGraphicsScene()
self._video_item = qtmw.QGraphicsVideoItem()
self._control_bar = ControlBar()
self._media_player = qtm.QMediaPlayer()
self._scene.addItem(self._video_item)
self._proxy_control_bar = self._scene.addWidget(self._control_bar)
self._proxy_control_bar.setFlag(qtw.QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations)
self.setScene(self._scene)
self._media_player.setVideoOutput(self._video_item)
self._media_player.setSource("video")
self._media_player.mediaStatusChanged.connect(self._media_player.play)
def showEvent(self, event) -> None:
qtc.QTimer.singleShot(100, lambda: self.fitInView(self._video_item, qtc.Qt.AspectRatioMode.KeepAspectRatio))
def resizeEvent(self, event) -> None:
self._proxy_control_bar.setGeometry(0, 0, self.viewport().width(), 100)
pos = qtc.QPoint(0, self.height() - self._proxy_control_bar.size().height())
self._proxy_control_bar.setPos(0, self.mapToScene(pos).y())
self.fitInView(self._video_item, qtc.Qt.AspectRatioMode.KeepAspectRatio)
app = qtw.QApplication()
view = View()
view.show()
app.exec()
I've been able to set the height of the widget to 100px, but using control_area.setGeometry(..., ..., self.viewport().width(), ...) sets the width to be a bit more than the video's width. And, for some reason, adding self._control_bar to the scene creates all this extra empty space around the two items, I have no idea why.
My questions are,
is there no way to get the actual size (specifically the width) of the video item after a fitInView call?
Because calling item.size() even after a fitInView call just returns the original size of the item, which I guess makes sense since only the view's view of the item was "fit in view" and the item itself is still the same.
How do I set the position of the control_bar to be where I want it to?
As seen in one of the videos below, the way I'm doing it right now does not accomplish it at all.
What's up with all the extra empty space?
How it looks:
Video with self._proxy_control_bar lines left in.
Video with self._proxy_control_bar lines commented out.
Related
I haven't worked with images in labels for a long time so I'm stuck with an issue - once resized a QPixmap (loaded inside a QLabel or similar widget) cannot return to a smaller (downsized) version of itself. This is particularly annoying when working with docked widgets in a QMainWindow or similar setting:
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from random import seed
from random import random
class CentralWidget(QWidget):
def __init__(self):
QWidget.__init__(self)
vb_layout = QVBoxLayout()
self.label = QLabel('Central Widget')
self.label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
vb_layout.addWidget(self.label)
self.setLayout(vb_layout)
class DockedWidget(QDockWidget):
class Widget(QWidget):
def __init__(self):
QWidget.__init__(self)
vb_layout = QVBoxLayout()
self.label = QLabel()
# Enable scaled contents, otherwise enjoy artifacts and visual glitches
self.label.setScaledContents(True)
self.rimg = QImage(self.width(),self.height(), QImage.Format_Grayscale8)
self.rimg.fill(Qt.black)
print(self.rimg.width(), self.rimg.height())
for j in range(self.height()):
for i in range(self.width()):
r = round(random()* 255)
if r % 2 == 0:
self.rimg.setPixel(i, j, qRgb(255, 0, 0))
self.label.setPixmap(QPixmap.fromImage(self.rimg))
vb_layout.addWidget(self.label)
self.setLayout(vb_layout)
def resizeEvent(self, e: QResizeEvent) -> None:
super().resizeEvent(e)
preview = self.label.pixmap()
# FIXME Trying to figure out a way to scale image inside label up and down
self.label.setPixmap(preview.scaled(self.label.width(),self.label.height(),Qt.KeepAspectRatio))
def __init__(self):
QDockWidget.__init__(self)
self.setWindowTitle('Docked Widget')
self.widget = DockedWidget.Widget()
self.setWidget(self.widget)
class MyWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setGeometry(300, 100, 270, 100)
self.setWindowTitle('Test')
dockedwidget = DockedWidget()
self.addDockWidget(Qt.LeftDockWidgetArea, dockedwidget)
widget = CentralWidget()
self.setCentralWidget(widget)
seed(1)
app = QApplication([])
win = MyWindow()
win.show()
app.exec_()
I've tried to link the pixmap's scaling to the parent label, which in terms should be controlled by the behaviour of the docked widget. Initially I was facing the issue that the image would stretch and create weird artifacts:
I figured out I had to enable scaled contents (QLabel.setScaledContents()) but I'm still facing the issue that I cannot go below the initial size of the image:
Minimum size restricts resizing beyond the initially set image size
Increasing the size is not a problem
I need to make the image capable of downsizing properly, otherwise it compromises the rest of the components in the layout in my actual setup. I'm thinking that the solution lies somewhere between the resize event and the size policy.
QLabel does not provide support for aspect ratio, and it normally only allows to scale it to sizes bigger than the image size (but this can be worked around using setMinimumSize(1, 1)).
While you could create a subclass that actually paints the proper aspect ratio no matter of the widget size, it's often not necessary, as you could use a QGraphicsView and a basic pixmap item.
class ImageWidget(QGraphicsView):
def __init__(self):
super().__init__()
self.setFrameShape(0)
self.setStyleSheet('''
QGraphicsView {
background: transparent;
}
''')
rimg = QImage(640, 480, QImage.Format_Grayscale8)
rimg.fill(Qt.black)
for j in range(480):
for i in range(640):
r = round(random()* 255)
if r % 2 == 0:
rimg.setPixel(i, j, qRgb(255, 0, 0))
scene = QGraphicsScene()
self.setScene(scene)
self.pixmapItem = scene.addPixmap(QPixmap.fromImage(rimg))
def resizeEvent(self, event):
super().resizeEvent(event)
self.fitInView(self.pixmapItem, Qt.KeepAspectRatio)
class DockedWidget(QDockWidget):
def __init__(self):
QDockWidget.__init__(self)
self.setWindowTitle('Docked Widget')
self.widget = ImageWidget()
self.setWidget(self.widget)
Note: 1. use nested classes only when actually necessary, otherwise they only make the code cumbersome and difficult to read; 2. all widgets have a default size when created (640x480, or 100x30 for widgets created with a parent), so using self.width() and self.height() is pointless.
Could be a either a Qt bug, or a bug in my app, I am hoping someone can help me diagnose this.
I am building a PyQt5 application with an interface built using qtdesigner and pyuic5.
I have to QSpinBox widgets in my window, which I connect to two methods like this:
self.img1IndexBox.valueChanged.connect(self.changeImage1)
self.img2IndexBox.valueChanged.connect(self.changeImage2)
Everything seems to work fine in the application, except for one thing: If I scroll the mouse over the spinbox, I can make it increment the value. If I change the value with text, all works fine. If I use keyboard arrows on the spinbox, it works fine. But if I click on either the up or down arrows from the spinbox, I get get two changeValue events, a double increment. Like I clicked twice. In fact, it even looks from the animation that it is creating an event for the downpress, and another when the button goes back up.
Could this be just a library bug, or what could be causing this in my program? How could I debug this?
You might be able to prevent that double thing by setting spinbox enable to false.
then make it enable to true after processing large data.
Upon clicking the arrow up/down
on_valuechanged
ui->spinbox->setEnabled(false);
then set to true before the function on_valuechanged ends.
ui->spinbox->setEnabled(true);
Apparently the problem is the event is triggering a very long routine, this delays the "button release" event, and it is enough time to make the system think the user is actually holding the button, generating more events... But I would still be interested in learning what would be a good walk-around. Would there be a nice pyqt-onic way to start a thread for that method?
http://www.qtcentre.org/archive/index.php/t-43078.html
Instead of using valueChanged.connect use editingFinished.connect it will make sure the function is called only after value is provided.
PyQt
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDoubleSpinBox, QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QSpinBox
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
# def valueChanged(self, value):
# print(value)
def valueChanged(self):
print(f"Value changed new value is : {self.spinBox.value()}")
def initUI(self):
self.setGeometry(0, 0, 200, 100)
self.layout = QVBoxLayout()
self.spinBox = QSpinBox()
self.spinBox.setAlignment(Qt.AlignCenter)
self.spinBox.setRange(0, 1000)
# self.spinBox.valueChanged.connect(self.valueChanged)
self.spinBox.editingFinished.connect(self.valueChanged)
self.layout.addWidget(self.spinBox)
self.setLayout(self.layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec())
PySide
import sys
from PySide6.QtWidgets import QDoubleSpinBox, QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QSpinBox
from PySide6.QtGui import Qt
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
# def valueChanged(self, value):
# print(value)
def valueChanged(self):
print(f"Value changed new value is : {self.spinBox.value()}")
def initUI(self):
self.setGeometry(0, 0, 200, 100)
self.layout = QVBoxLayout()
self.spinBox = QSpinBox()
self.spinBox.setAlignment(Qt.AlignCenter)
self.spinBox.setRange(0, 1000)
# self.spinBox.valueChanged.connect(self.valueChanged)
self.spinBox.editingFinished.connect(self.valueChanged)
self.layout.addWidget(self.spinBox)
self.setLayout(self.layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec())
I tried using the Qt documentation example to restrict the rectangle to the area of the scene but it still fails, someone has an alternative to do this?
My code, the QGraphicsView instance was created in Qt Desginer:
# -*- coding: utf-8 -*-
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
from screen import *
class MovableItem(QGraphicsRectItem):
def __init__(self, rectang, *args, **kwargs):
QGraphicsRectItem.__init__(self, rectang, *args, **kwargs)
self.setFlags(QGraphicsItem.ItemIsMovable |
QGraphicsItem.ItemSendsGeometryChanges)
self.pen = QPen(Qt.darkMagenta)
self.pen.setWidth(4)
self.setPen(self.pen)
def itemChange(self, change, value):
if change == QGraphicsItem.ItemPositionChange and self.scene():
# value is the new position.
self.newPos = value.toPointF()
self.rect = self.scene().sceneRect()
if not(self.rect.contains(self.newPos)):
# Keep the item inside the scene rect.
self.newPos.setX(min(self.rect.right(), max(self.newPos.x(), self.rect.left())))
self.newPos.setY(min(self.rect.bottom(), max(self.newPos.y(), self.rect.top())))
return self.newPos
return QGraphicsRectItem.itemChange(self, change, value)
class Main(QWidget, Ui_Form):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
self.setupUi(self)
self.scene = QGraphicsScene()
self.cena.setScene(self.scene)
self.scene.addPixmap(QPixmap("01.png"))
self. graph = MovableItem(2, 2, 300, 150)
self.scene.addItem(self.graph)
def showEvent(self, event):
self.cena.fitInView(self.scene.sceneRect(), Qt.IgnoreAspectRatio)
app = QApplication(sys.argv)
window = Main()
window.show()
sys.exit(app.exec_())
First:
Use setSceneRect() in your main Main(), to set the size of the scene.
Second:
Actually the example of the documentation is wrong, therefore, to adjust the rectangle to the scene, delete this if and subtract, in min, the parameters right and bottom by the rectangle dimensions right and bottom in setX and setY. Replace this part of your code:
if not(self.rect.contains(self.newPos)):
# Keep the item inside the scene rect.
self.newPos.setX(min(self.rect.right(), max(self.newPos.x(), self.rect.left())))
self.newPos.setY(min(self.rect.bottom(), max(self.newPos.y(), self.rect.top())))
return self.newPos
For:
self.newPos.setX(min(self.rect.right()-self.boundingRect().right(), max(self.newPos.x(), self.rect.left())))
self.newPos.setY(min(self.rect.bottom()-self.boundingRect().bottom(), max(self.newPos.y(), self.rect.top())))
return self.newPos
Finally I am able to create a chrome like tab in Qt/PyQt QMainWindow. After unsuccessfully trying to port this Qt C++ non client area painting code, I revise my thinking to be this way : trick visually by displaying a free floating QFrame that get resized and moved together with main window. Surely this is not a perfect solution (for example this code still don't solve when to disable topmost hint where the another application is on top of the main application window, but I think that's quite easy to solve)
What I want to ask in this page, is how to keep the click action on this QFrame window button from stealing focus from main window? At the moment I simply reactivate the main window when click action does occur. But it creates flashing effect on the mainwindow titlebar. I believe this SO page gives the answer, but I haven't been able to create a successful result from this C++ code:
HWND winHandle = (HWND)winId();
ShowWindow(winHandle, SW_HIDE);
SetWindowLong(winHandle, GWL_EXSTYLE, GetWindowLong(winHandle, GWL_EXSTYLE)
| WS_EX_NOACTIVATE | WS_EX_APPWINDOW);
ShowWindow(winHandle, SW_SHOW);
Into this PyQt code:
def no_focus(self):
import ctypes, win32con, win32gui
dc = win32gui.GetWindowDC(self.winId())
user32 = ctypes.windll.user32
user32.SetWindowLongW(dc, win32con.GWL_EXSTYLE, user32.GetWindowLongW(dc, win32con.GWL_EXSTYLE) | win32con.WS_EX_NOACTIVATE | win32con.WS_EX_APPWINDOW)
Would love to let you see and test the fully functional code below:
__author__ = 'Eko Wibowo'
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class FSTabHeader(QFrame):
def __init__(self, parent):
super(FSTabHeader, self).__init__(None)
self.mainwindow = parent
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.SplashScreen)
self.setFocusPolicy(Qt.NoFocus)
self.setAttribute(Qt.WA_ShowWithoutActivating)
layout = QHBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
tab_text = 'Click me'
self.btn_tab = QPushButton(self)
self.btn_tab.setStyleSheet('border:1px')
self.btn_tab.setContentsMargins(0,0,0,0)
self.btn_tab.setText(tab_text)
self.btn_tab.setMinimumHeight(25 + 1)
self.btn_tab.setMaximumHeight(25 + 1)
self.btn_tab.setMinimumWidth(60)
self.btn_tab.setMaximumWidth(60)
self.btn_tab.setCursor(Qt.PointingHandCursor)
self.btn_tab.clicked.connect(self.dummy)
layout.addWidget(self.btn_tab)
self.setLayout(layout)
self.show()
def dummy(self):
print 'it create flashes effect on mainwindow titlebar'
self.mainwindow.activateWindow()
def no_focus(self):
import ctypes, win32con, win32gui
dc = win32gui.GetWindowDC(self.winId())
user32 = ctypes.windll.user32
user32.SetWindowLongW(dc, win32con.GWL_EXSTYLE, user32.GetWindowLongW(dc, win32con.GWL_EXSTYLE) | win32con.WS_EX_NOACTIVATE | win32con.WS_EX_APPWINDOW)
def adjust_position(self):
top_left = self.mainwindow.mapToGlobal(self.mainwindow.rect().topLeft())
self.move(top_left.x() + 20 + 5, top_left.y() - self.height() + 1)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.tab_header = FSTabHeader(self)
self.tab_header.no_focus()
def resizeEvent(self, *args, **kwargs):
self.tab_header.adjust_position()
def moveEvent(self, *args, **kwargs):
self.tab_header.adjust_position()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
mainWindow = MainWindow(None)
mainWindow.show()
app.exec_()
Any suggestions?
I am faced with this problem and being a Qt noob am not able to fix it.
Basically, I instantiated a QToolButton and parented it to QTreeWidget. The QTreeWidget is inside a vertical layout and when I try to change the position of the tool button inside the QTreeWidget using QTreeWidget.size() it gives me very unexpected and wrong results.
Can anyone help me with this? Will deeply appreciate the help. Thanks!
You haven't posted any examples of what you are actually doing, but here is how to attach a button to the lower right of the tree widget:
Edit: I have replaced my answer after seeing that you want to composite the widget OVER the tree
Using an eventFilter
from PyQt4 import QtCore, QtGui
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.resize(640,480)
self.layout = QtGui.QVBoxLayout(self)
self.layout.setSpacing(0)
self.tree = QtGui.QTreeWidget(self)
self.tree.installEventFilter(self)
self.layout.addWidget(self.tree)
self.button = QtGui.QToolButton(self.tree)
self.button.setText("FOO")
self.button.setMinimumSize(100, 30)
def eventFilter(self, obj, event):
if obj is self.tree and event.type() == event.Resize:
self.alignTreeButton()
return False
def alignTreeButton(self):
padding = QtCore.QSize(5,5) # optional
newSize = self.tree.size() - self.button.size() - padding
self.button.move(newSize.width(), newSize.height())
if __name__ == "__main__":
app = QtGui.QApplication([])
w = Widget()
w.show()
w.raise_()
app.exec_()
The button is just parented to the tree, and we install the event filter on the tree to catch resize events. Once the tree is resized, we take its size, subtract the size of the button, and then move the button.
Using composition
I believe its more efficient to actually subclass the QTreeWidget, compose it with the QToolButton as a member, and then overload the resizeEvent() locally to handle the resize. First off this makes the behavior handling local to the TreeWidget, which is cleaner. Also, I believe it reduces the overhead that an EventFilter would add to your main window. The eventFiler would be a python callable that is called many more times because of it handling every event for the object. Whereas the local resizeEvent() for the TreeWidget is only called during the resize.
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.resize(640,480)
self.layout = QtGui.QVBoxLayout(self)
self.layout.setSpacing(0)
self.tree = TreeWidget(self)
self.layout.addWidget(self.tree)
class TreeWidget(QtGui.QTreeWidget):
def __init__(self, *args, **kwargs):
super(TreeWidget, self).__init__(*args, **kwargs)
self.button = QtGui.QToolButton(self)
self.button.setText("FOO")
self.button.setMinimumSize(100, 30)
def resizeEvent(self, event):
super(TreeWidget, self).resizeEvent(event)
self.alignTreeButton()
def alignTreeButton(self):
padding = QtCore.QSize(5,5) # optional
newSize = self.size() - self.button.size() - padding
self.button.move(newSize.width(), newSize.height())