Related
I am using PyQt and I'm trying to re-implement a QGraphicsTextItem, but it seems I'm missing something.
I would like to make the NodeTag item's text editable. I have tried setting flags such as Qt.TextEditorInteraction and QGraphicsItem.ItemIsMovable , but those seem to be ignored...
Here is a Minimal Reproducible Example :
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPen
class NodeTag(QGraphicsTextItem):
def __init__(self,text):
QGraphicsTextItem.__init__(self,text)
self.text = text
self.setPos(0,0)
self.setTextInteractionFlags(Qt.TextEditorInteraction)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True) # All these flags are ignored...
# self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def boundingRect(self):
return QRectF(0,0,80,25)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
painter.drawText(self.boundingRect(),self.text)
def mousePressEvent(self, event):
print("CLICK!")
# self.setTextInteractionFlags(Qt.TextEditorInteraction) # make text editable on click
# self.setFocus()
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
tagItem = NodeTag("myText") # create a NodeTag item
self.addItem(tagItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__() # create default constructor for QWidget
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
As you can see I have tried overriding the mousePressEvent and setting flags there too, but no luck so far.
Any help appreciated!
All QGraphicsItem subclasses have a paint method, and all items that paint some contents have that method overridden so that they can actually paint themselves.
The mechanism is the same as standard QWidgets, for which there is a paintEvent (the difference is that paint of QGraphicsItem receives an already instanciated QPainter), so if you want to do further painting other than what the class already provides, the base implementation must be called.
Consider that painting always happen from bottom to top, so everything that needs to be drawn behind the base painting has to be done before calling super().paint(), and everything that is going to be drawn in front of the default painting has to be placed after.
Depending on the situation, overriding might require that the default base implementation is called anyway, and that's important in your case for boundingRect too. QGraphicsTextItem automatically resizes itself when its contents change, so you should not always return a fixed QRect. If you need to have a minimum size, the solution is to merge a minimum rectangle with those provided by the default boundingRect() function.
Then, editing on a QGraphicsTextItem happens when the item gets focused, but since you also want to be able to move the item, things get trickier as both actions are based on mouse clicks. If you want to be able to edit the text with a single click, the solution is to make the item editable only when the mouse button has been released and has not been moved by some amount of pixels (the startDragDistance() property), otherwise the item is moved with the mouse. This obviously makes the ItemIsMovable flag useless, as we're going to take care of the movement internally.
Finally, since a minimum size is provided, we also need to override the shape() method in order to ensure that collision and clicks are correctly mapped, and return a QPainterPath that includes the whole bounding rect (for normal QGraphicsItem that should be the default behavior, but that doesn't happen with QGraphicsRectItem).
Here's a full implementation of what described above:
class NodeTag(QGraphicsTextItem):
def __init__(self, text):
QGraphicsTextItem.__init__(self, text)
self.startPos = None
self.isMoving = False
# the following is useless, not only because we are leaving the text
# painting to the base implementation, but also because the text is
# already accessible using toPlainText() or toHtml()
#self.text = text
# this is unnecessary too as all new items always have a (0, 0) position
#self.setPos(0, 0)
def boundingRect(self):
return super().boundingRect() | QRectF(0, 0, 80, 25)
def paint(self, painter, option, widget):
# draw the border *before* (as in "behind") the text
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
super().paint(painter, option, widget)
def shape(self):
shape = QPainterPath()
shape.addRect(self.boundingRect())
return shape
def focusOutEvent(self, event):
# this is required in order to allow movement using the mouse
self.setTextInteractionFlags(Qt.NoTextInteraction)
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.startPos:
delta = event.pos() - self.startPos
if (self.isMoving or
delta.manhattanLength() >= QApplication.startDragDistance()):
self.setPos(self.pos() + delta)
self.isMoving = True
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if (not self.isMoving and
self.textInteractionFlags() != Qt.TextEditorInteraction):
self.setTextInteractionFlags(Qt.TextEditorInteraction)
self.setFocus()
# the following lines are used to correctly place the text
# cursor at the mouse cursor position
cursorPos = self.document().documentLayout().hitTest(
event.pos(), Qt.FuzzyHit)
textCursor = self.textCursor()
textCursor.setPosition(cursorPos)
self.setTextCursor(textCursor)
super().mouseReleaseEvent(event)
self.startPos = None
self.isMoving = False
As a side note, remember that QGraphicsTextItem supports rich text formatting, so even if you want more control on the text painting process you should not use QPainter.drawText(), because you'd only draw the plain text. In fact, QGraphicsTextItem draws its contents using the drawContents() function of the underlying text document.
Try it:
...
class NodeTag(QGraphicsTextItem):
def __init__(self, text, parent=None):
super(NodeTag, self).__init__(parent)
self.text = text
self.setPlainText(text)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
def focusOutEvent(self, event):
self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
super(NodeTag, self).focusOutEvent(event)
def mouseDoubleClickEvent(self, event):
if self.textInteractionFlags() == QtCore.Qt.NoTextInteraction:
self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
super(NodeTag, self).mouseDoubleClickEvent(event)
def paint(self,painter,option,widget):
painter.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
painter.drawRect(self.boundingRect())
# painter.drawText(self.boundingRect(),self.text)
super().paint(painter, option, widget)
...
I am trying to set the vertical and horizontal scroll bars initially moved inside a QGraphicsScene widget. The following code should move the bars and set them in the middle, but they are not moved:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(1000, 1000)
self.setFixedSize(1000, 1000)
self.scene = QtWidgets.QGraphicsScene(self)
self.view = QtWidgets.QGraphicsView(self.scene)
# This two lines should move the scroll bar
self.view.verticalScrollBar().setValue(500)
self.view.horizontalScrollBar().setValue(500)
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
self.scene.addWidget(self.diedrico)
self.setCentralWidget(self.view)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_R:
self.view.setTransform(QtGui.QTransform())
elif event.key() == QtCore.Qt.Key_Plus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
tr = self.view.transform() * scale_tr
self.view.setTransform(tr)
elif event.key() == QtCore.Qt.Key_Minus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self.view.transform() * scale_inverted
self.view.setTransform(tr)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
I could move the bars when I used a scroll area such as in this question
The answer given by #S.Nick works fine, but I'd like to add some insight about why you are facing this issue and what's happening "under the hood".
First of all, in your code you try to set the values of the scroll bars before adding any object to the scene.
At that point, you just created the view and the scene. The view widget has not been shown (so it doesn't "know" its actual size yet), and the scene is empty, meaning that the sceneRect is null, as in 0 width and 0 height: in this scenario, the scroll bars have a maximum value of 0, and setting any value won't give any result.
NOTE: There is a very important aspect to keep in mind: unless
explicitly declared or requested, the sceneRect of a
QGraphicsScene is always null until a view shows it. And by
"requested" I mean that even just calling scene.sceneRect() is
enough to ensure that the scene actually and finally "knows" its
extent.
After trying to set the scroll bars (with no results), you added the widget to the scene. The problem is that a view (which is a QAbstractScrollArea descendant) only updates its scrollbars as soon as it's actually mapped on the screen.
This is a complex "path" that starts from showing the main parent window (if any), which, according to its contents resizes itself and, again, resizes its contents if they require it, eventually based on their [nested widget] size policies. Only then, the view "decides" if scrollbars are needed, and eventually sets their maximum. And, only then you can actuall set a value for those scroll bars, and that's because only then the view "asks" the scene about its sceneRect.
This also (partially) explains why the view behaves in different way than a standard scroll area: widgets have a sizeHint that is used by the QWidget that contains them inside the scroll area, and, theoretically, their size is mapped as soon as they're created. But. this depends on their size hints and policies, so you cannot guarantee the actual scroll area contents size until it's finally mapped/shown; long story short: it "works", but not perfectly - at least not until everything has finally been shown.
A test example
There are different ways to solve your problem, according to your needs and implementation.
Set the sceneRect independently, even before adding any object to the scene (but if those objects boundaries go outside the scene, you'll face some inconsistency)
Call scene.sceneRect() as explained above, after adding all objects
Set the scoll bars only after the view has been shown and resized
I've prepared an example that shows the three situations explained above. It will create a new view and update its scrollbars at different points according to the checkboxes, to show how differently they behave. Note that when setting the sceneRect I used a rectangle smaller than the widget size to better display its behavior: you can see that the visual result of "Set scene rect" and "Check scene rect" is similar, but the scroll bar positions are different.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class TestView(QtWidgets.QGraphicsView):
def __init__(self, setRect=False, checkScene=False, showEventCheck=False):
super(TestView, self).__init__()
self.setFixedSize(800, 800)
scene = QtWidgets.QGraphicsScene()
self.setScene(scene)
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
scene.addWidget(self.diedrico)
if setRect:
scene.setSceneRect(0, 0, 1500, 1500)
elif checkScene:
scene.sceneRect()
self.showEventCheck = showEventCheck
if not showEventCheck:
self.scroll()
def scroll(self):
self.verticalScrollBar().setValue(500)
self.horizontalScrollBar().setValue(500)
def showEvent(self, event):
super(TestView, self).showEvent(event)
if not event.spontaneous() and self.showEventCheck:
self.scroll()
class ViewTester(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
self.setRectCheck = QtWidgets.QCheckBox('Set scene rect')
layout.addWidget(self.setRectCheck)
self.checkSceneCheck = QtWidgets.QCheckBox('Check scene rect')
layout.addWidget(self.checkSceneCheck)
self.showEventCheck = QtWidgets.QCheckBox('Scroll when shown')
layout.addWidget(self.showEventCheck)
showViewButton = QtWidgets.QPushButton('Show view')
layout.addWidget(showViewButton)
showViewButton.clicked.connect(self.showView)
self.view = None
def showView(self):
if self.view:
self.view.close()
self.view.deleteLater()
self.view = TestView(
setRect = self.setRectCheck.isChecked(),
checkScene = self.checkSceneCheck.isChecked(),
showEventCheck = self.showEventCheck.isChecked()
)
self.view.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
viewTester = ViewTester()
viewTester.show()
sys.exit(app.exec_())
Finally, remember that using absolute values for scrollbars is not a good idea. If you want to "center" the view, consider using centerOn (and its item based overload), or set values based on scrollBar.maximum()/2.
You want to set the value when the widget is not yet formed, make it a moment.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(1000, 1000)
self.setFixedSize(1000, 1000)
self.scene = QtWidgets.QGraphicsScene(self)
self.view = QtWidgets.QGraphicsView(self.scene)
# This two lines should move the scroll bar
QtCore.QTimer.singleShot(0, self.set_Value) # +++
self.diedrico = Diedrico()
self.diedrico.setFixedSize(2000, 2000)
self.scene.addWidget(self.diedrico)
self.setCentralWidget(self.view)
def set_Value(self): # +++
self.view.verticalScrollBar().setValue(500)
self.view.horizontalScrollBar().setValue(500)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_R:
self.view.setTransform(QtGui.QTransform())
elif event.key() == QtCore.Qt.Key_Plus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
tr = self.view.transform() * scale_tr
self.view.setTransform(tr)
elif event.key() == QtCore.Qt.Key_Minus:
scale_tr = QtGui.QTransform()
scale_tr.scale(1.5, 1.5)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self.view.transform() * scale_inverted
self.view.setTransform(tr)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
I'm working on an opensource markdown supported minimal note taking application for Windows/Linux. I'm trying to remove the title bar and add my own buttons. I want something like, a title bar with only two custom buttons as shown in the figure
Currently I have this:
I've tried modifying the window flags:
With not window flags, the window is both re-sizable and movable. But no custom buttons.
Using self.setWindowFlags(QtCore.Qt.FramelessWindowHint), the window has no borders, but cant move or resize the window
Using self.setWindowFlags(QtCore.Qt.CustomizeWindowHint), the window is resizable but cannot move and also cant get rid of the white part at the top of the window.
Any help appreciated. You can find the project on GitHub here.
Thanks..
This is my python code:
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, uic
import sys
import os
import markdown2 # https://github.com/trentm/python-markdown2
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QFont
simpleUiForm = uic.loadUiType("Simple.ui")[0]
class SimpleWindow(QtWidgets.QMainWindow, simpleUiForm):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.markdown = markdown2.Markdown()
self.css = open(os.path.join("css", "default.css")).read()
self.editNote.setPlainText("")
#self.noteView = QtWebEngineWidgets.QWebEngineView(self)
self.installEventFilter(self)
self.displayNote.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
#self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate:
print("widget window has gained focus")
self.editNote.show()
self.displayNote.hide()
elif event.type() == QtCore.QEvent.WindowDeactivate:
print("widget window has lost focus")
note = self.editNote.toPlainText()
htmlNote = self.getStyledPage(note)
# print(note)
self.editNote.hide()
self.displayNote.show()
# print(htmlNote)
self.displayNote.setHtml(htmlNote)
elif event.type() == QtCore.QEvent.FocusIn:
print("widget has gained keyboard focus")
elif event.type() == QtCore.QEvent.FocusOut:
print("widget has lost keyboard focus")
return False
The UI file is created in the following hierarchy
Here are the steps you just gotta follow:
Have your MainWindow, be it a QMainWindow, or QWidget, or whatever [widget] you want to inherit.
Set its flag, self.setWindowFlags(Qt.FramelessWindowHint)
Implement your own moving around.
Implement your own buttons (close, max, min)
Implement your own resize.
Here is a small example with move around, and buttons implemented. You should still have to implement the resize using the same logic.
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addStretch(-1)
self.setMinimumSize(800,400)
self.setWindowFlags(Qt.FramelessWindowHint)
self.pressing = False
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
print(self.parent.width())
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("My Own Bar")
btn_size = 35
self.btn_close = QPushButton("x")
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(btn_size,btn_size)
self.btn_close.setStyleSheet("background-color: red;")
self.btn_min = QPushButton("-")
self.btn_min.clicked.connect(self.btn_min_clicked)
self.btn_min.setFixedSize(btn_size, btn_size)
self.btn_min.setStyleSheet("background-color: gray;")
self.btn_max = QPushButton("+")
self.btn_max.clicked.connect(self.btn_max_clicked)
self.btn_max.setFixedSize(btn_size, btn_size)
self.btn_max.setStyleSheet("background-color: gray;")
self.title.setFixedHeight(35)
self.title.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_min)
self.layout.addWidget(self.btn_max)
self.layout.addWidget(self.btn_close)
self.title.setStyleSheet("""
background-color: black;
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
def btn_max_clicked(self):
self.parent.showMaximized()
def btn_min_clicked(self):
self.parent.showMinimized()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
Here are some tips:
Option 1:
Have a QGridLayout with widget in each corner and side(e.g. left, top-left, menubar, top-right, right, bottom-right, bottom and bottom left)
With the approach (1) you would know when you are clicking in each border, you just got to define each one size and add each one on their place.
When you click on each one treat them in their respective ways, for example, if you click in the left one and drag to the left, you gotta resize it larger and at the same time move it to the left so it will appear to be stopped at the right place and grow width.
Apply this reasoning to each edge, each one behaving in the way it has to.
Option 2:
Instead of having a QGridLayout you can detect in which place you are clicking by the click pos.
Verify if the x of the click is smaller than the x of the moving pos to know if it's moving left or right and where it's being clicked.
The calculation is made in the same way of the Option1
Option 3:
Probably there are other ways, but those are the ones I just thought of. For example using the CustomizeWindowHint you said you are able to resize, so you just would have to implement what I gave you as example. BEAUTIFUL!
Tips:
Be careful with the localPos(inside own widget), globalPos(related to your screen). For example: If you click in the very left of your left widget its 'x' will be zero, if you click in the very left of the middle(content)it will be also zero, although if you mapToGlobal you will having different values according to the pos of the screen.
Pay attention when resizing, or moving, when you have to add width or subtract, or just move, or both, I'd recommend you to draw on a paper and figure out how the logic of resizing works before implementing it out of blue.
GOOD LUCK :D
While the accepted answer can be considered valid, it has some issues.
using setGeometry() is not appropriate (and the reason for using it was wrong) since it doesn't consider possible frame margins set by the style;
the position computation is unnecessarily complex;
resizing the title bar to the total width is wrong, since it doesn't consider the buttons and can also cause recursion problems in certain situations (like not setting the minimum size of the main window); also, if the title is too big, it makes impossible to resize the main window;
buttons should not accept focus;
setting a layout creates a restraint for the "main widget" or layout, so the title should not be added, but the contents margins of the widget should be used instead;
I revised the code to provide a better base for the main window, simplify the moving code, and add other features like the Qt windowTitle() property support, standard QStyle icons for buttons (instead of text), and proper maximize/normal button icons. Note that the title label is not added to the layout.
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.titleBar = MyBar(self)
self.setContentsMargins(0, self.titleBar.height(), 0, 0)
self.resize(640, self.titleBar.height() + 480)
def changeEvent(self, event):
if event.type() == event.WindowStateChange:
self.titleBar.windowStateChanged(self.windowState())
def resizeEvent(self, event):
self.titleBar.resize(self.width(), self.titleBar.height())
class MyBar(QWidget):
clickPos = None
def __init__(self, parent):
super(MyBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.Shadow)
# alternatively:
# palette = self.palette()
# palette.setColor(palette.Window, Qt.black)
# palette.setColor(palette.WindowText, Qt.white)
# self.setPalette(palette)
layout = QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.addStretch()
self.title = QLabel("My Own Bar", self, alignment=Qt.AlignCenter)
# if setPalette() was used above, this is not required
self.title.setForegroundRole(QPalette.Light)
style = self.style()
ref_size = self.fontMetrics().height()
ref_size += style.pixelMetric(style.PM_ButtonMargin) * 2
self.setMaximumHeight(ref_size + 2)
btn_size = QSize(ref_size, ref_size)
for target in ('min', 'normal', 'max', 'close'):
btn = QToolButton(self, focusPolicy=Qt.NoFocus)
layout.addWidget(btn)
btn.setFixedSize(btn_size)
iconType = getattr(style,
'SP_TitleBar{}Button'.format(target.capitalize()))
btn.setIcon(style.standardIcon(iconType))
if target == 'close':
colorNormal = 'red'
colorHover = 'orangered'
else:
colorNormal = 'palette(mid)'
colorHover = 'palette(light)'
btn.setStyleSheet('''
QToolButton {{
background-color: {};
}}
QToolButton:hover {{
background-color: {}
}}
'''.format(colorNormal, colorHover))
signal = getattr(self, target + 'Clicked')
btn.clicked.connect(signal)
setattr(self, target + 'Button', btn)
self.normalButton.hide()
self.updateTitle(parent.windowTitle())
parent.windowTitleChanged.connect(self.updateTitle)
def updateTitle(self, title=None):
if title is None:
title = self.window().windowTitle()
width = self.title.width()
width -= self.style().pixelMetric(QStyle.PM_LayoutHorizontalSpacing) * 2
self.title.setText(self.fontMetrics().elidedText(
title, Qt.ElideRight, width))
def windowStateChanged(self, state):
self.normalButton.setVisible(state == Qt.WindowMaximized)
self.maxButton.setVisible(state != Qt.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clickPos = event.windowPos().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
self.window().move(event.globalPos() - self.clickPos)
def mouseReleaseEvent(self, QMouseEvent):
self.clickPos = None
def closeClicked(self):
self.window().close()
def maxClicked(self):
self.window().showMaximized()
def normalClicked(self):
self.window().showNormal()
def minClicked(self):
self.window().showMinimized()
def resizeEvent(self, event):
self.title.resize(self.minButton.x(), self.height())
self.updateTitle()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
layout = QVBoxLayout(mw)
widget = QTextEdit()
layout.addWidget(widget)
mw.show()
mw.setWindowTitle('My custom window with a very, very long title')
sys.exit(app.exec_())
This is for the people who are going to implement custom title bar in PyQt6 or PySide6
The below changes should be done in the answer given by #musicamante
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# self.clickPos = event.windowPos().toPoint()
self.clickPos = event.scenePosition().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
# self.window().move(event.globalPos() - self.clickPos)
self.window().move(event.globalPosition().toPoint() - self.clickPos)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
# sys.exit(app.exec_())
sys.exit(app.exec())
References:
QMouseEvent.globalPosition(),
QMouseEvent.scenePosition()
This method of moving Windows with Custom Widget doesn't work with WAYLAND. If anybody has a solution for that please post it here for future reference
Working functions for WAYLAND and PyQT6/PySide6 :
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._move()
return super().mousePressEvent(event)
def _move(self):
window = self.window().windowHandle()
window.startSystemMove()
Please check.
I have a setup where two QGraphicViews display a single QGraphicsScene. One of these views is an overview the other the detail. Imagine something like:
The rectangle marking the current boundaries of the detailed view is part of the scene. It is the white rectangle in the upper view, which I will call in the text below as "bounding-box".
What I want is to be able to click in the overview- QGraphicsView and drag the bounding-box around to trigger a scrolling of the detail- QGraphicsView. Obviously, the bounding-box has to be only clickable in the overview- QGraphicsView, otherwise I would never be able to do manipulations in the detail- QGraphicsView, because the bounding-box covers the entire detail view.
So how can I make a QGraphicsItem be selectable only from a single QGraphicsView or, alternatively, how do I "insert" a QGraphicsItem only into a single QGraphicsView? Can I perhaps nest QGraphicsScenes so that one is the copy of another plus some extra items?
Extending my other answer which only concentrates on the movable QGraphicsItem I made an example specifically for your task.
from PySide import QtGui, QtCore
# special GraphicsRectItem that is aware of its position and does something if the position is changed
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
def __init__(self, callback=None):
super().__init__()
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
self.callback = callback
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
self.callback(value)
return super().itemChange(change, value)
app = QtGui.QApplication([])
# the scene with some rectangles
scene = QtGui.QGraphicsScene()
scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))
window = QtGui.QWidget()
# put two graphicsviews into the window with different scaling for each
layout = QtGui.QVBoxLayout(window)
v1 = QtGui.QGraphicsView(scene)
v1.setFixedSize(500, 100)
v1.scale(0.5, 0.5)
v1.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
v1.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
layout.addWidget(v1)
v2 = QtGui.QGraphicsView(scene)
v2.setFixedSize(500, 500)
v2.scale(5, 5)
v2.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
v2.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
layout.addWidget(v2)
# the tracker rectangle
tracker = MovableGraphicsRectItem(lambda pos: v2.setSceneRect(pos.x(), pos.y(), 100, 100))
tracker.setRect(0, 0, 100, 100)
v2.setSceneRect(0, 0, 100, 100)
tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
scene.addItem(tracker)
window.show()
app.exec_()
You don't need to have Items that are only visible in one view or the other, you simply restrict the scene rectangle of one view to inside the draggable rectangle in the scene that is visible and draggable in the other view. See the image.
I really like this idea and am trying to generalise it to create a widget which you pass the 'main view' to and it creates an overview which you can use to pan and zoom in. Unfortunately I haven't got it working yet and don't have time to work on it right now but thought I would share the progress so far.
Here is the widget code:
"""
Overview widget
"""
from PyQt4 import QtGui, QtCore
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
'''special GraphicsRectItem that is aware of its position and does
something if the position is changed'''
def __init__(self, callback=None):
super(MovableGraphicsRectItem, self).__init__()
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
self.callback = callback
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
self.callback(value)
return super(MovableGraphicsRectItem, self).itemChange(change, value)
def activate(self):
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
def deactivate(self):
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, False)
self.setFlag(QtGui.QGraphicsItem.ItemSendsScenePositionChanges, False)
self.setCursor(QtCore.Qt.ArrowCursor)
class MouseInsideFilterObj(QtCore.QObject):
def __init__(self, enterCallback, leaveCallback):
QtCore.QObject.__init__(self)
self.enterCallback = enterCallback
self.leaveCallback = leaveCallback
def eventFilter(self, obj, event):
if event.type() == 10: # QtCore.QEvent.Type.Enter:
self.enterCallback(obj)
print('Enter event')
if event.type() == 11: # QtCore.QEvent.Type.Leave:
self.leaveCallback(obj)
print('Leave event')
return False
class Overview(QtGui.QGraphicsView):
'''provides a view that shows the entire scene and shows the area that
the main view is zoomed to. Alows user to move the view area around and
change the zoom level'''
def __init__(self, mainView):
QtGui.QGraphicsView.__init__(self)
self.setWindowTitle('Overview')
self.resize(QtCore.QSize(400, 300))
self._mainView = mainView
self.setScene(mainView.scene())
mouseFilter = MouseInsideFilterObj(self.enterGV, self.leaveGV)
self.viewport().installEventFilter(mouseFilter)
self._tracker = MovableGraphicsRectItem(
lambda pos: self._mainView.setSceneRect(
QtCore.QRectF(self._mainView.viewport().geometry())))
self._tracker.setRect(self._getMainViewArea_())
self._tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
self.scene().addItem(self._tracker)
def _getMainViewArea_(self):
mainView = self._mainView
visibleSceneRect = mainView.mapToScene(
mainView.viewport().geometry()).boundingRect()
return visibleSceneRect
def resizeEvent(self, event):
self.fitInView(self.sceneRect(), QtCore.Qt.KeepAspectRatio)
def leaveGV(self, gv):
if gv is self.overview:
print('exited overview')
self.tracker.deactivate()
def enterGV(self, gv):
if gv is self.overview:
print('using overview')
self.tracker.activate()
and here is the test script code:
import sys
from PyQt4 import QtGui, QtCore
import overviewWidget as ov
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
# the scene with some rectangles
scene = QtGui.QGraphicsScene()
scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))
# the main view
mainView = QtGui.QGraphicsView(scene)
mainView.resize(600, 400)
mainView.update()
mainView.show()
# the overview
overview = ov.Overview(mainView)
overview.update()
overview.show()
sys.exit(app.exec_())
QGraphicsItems have by default some of their abilities disabled to maximize performance. By enabling these abilities you can make them movable and you can make them aware of their position. Ideally one would then use the Signal/Slot mechanism to notify someone else of changes but again for performance reason QGraphicsItems are not inheriting from QObject. However sending events or manually calling callbacks are always possible.
You have to:
Enable flags QGraphicsItem.ItemIsMovable and QGraphicsItem.ItemSendsScenePositionChanges of your QGraphicsItem
Provide a custom implementation of method itemChange(change, value) and therein listen to QGraphicsItem.ItemPositionChange changes.
Act accordingly to these changes (in your case change the detailed view).
A small example:
from PySide import QtGui, QtCore
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
"""
A QGraphicsRectItem that can be moved and is aware of its position.
"""
def __init__(self):
super().__init__()
# enable moving and position tracking
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
# sets a non-default cursor
self.setCursor(QtCore.Qt.PointingHandCursor)
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange:
print(value)
return super().itemChange(change, value)
app = QtGui.QApplication([])
# create our movable rectangle
rectangle = MovableGraphicsRectItem()
rectangle.setRect(0, 0, 100, 100)
# create a scene and add our rectangle
scene = QtGui.QGraphicsScene()
scene.addItem(rectangle)
# create view, set fixed scene rectangle and show
view = QtGui.QGraphicsView(scene)
view.setSceneRect(0, 0, 600, 400)
view.show()
app.exec_()
In this example (Python 3.X) you can drag the rectangle around and the changing positions are printed to the console.
Some more comments:
You have two views and two associated scenes.
Their display is partly overlapping but this is not a problem because the top view will always consume all mouse events.
In order to change something in the other view you just have to send an event from the overriden itemChange method or call a callback.
You could also add Signal/Slot ability by inheriting from both, QGraphicsRectItem and QObject and then define a signal and emit it.
If by chance you also wanted a movable and position aware ellipse or other item you need to create your custom classes for each xxxItem class. I stumbled upon this problem several times and think it might be a disadvantage of the design.
Extending the answer of Trilarion, I was able to solve the problem, by installing a Eventfilter on the overview QgraphcisView. On the Enter event, the dragging is enabled, on the Leave event the dragging is disabled.
from PySide import QtGui, QtCore
# special GraphicsRectItem that is aware of its position and does something if the position is changed
class MovableGraphicsRectItem(QtGui.QGraphicsRectItem):
def __init__(self, callback=None):
super(MovableGraphicsRectItem, self).__init__()
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
self.callback = callback
def itemChange(self, change, value):
if change == QtGui.QGraphicsItem.ItemPositionChange and self.callback:
self.callback(value)
return super(MovableGraphicsRectItem, self).itemChange(change, value)
def activate(self):
self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.PointingHandCursor)
def deactivate(self):
self.setFlags(not QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemSendsScenePositionChanges)
self.setCursor(QtCore.Qt.ArrowCursor)
class MouseInsideFilterObj(QtCore.QObject):#And this one
def __init__(self, enterCallback, leaveCallback):
QtCore.QObject.__init__(self)
self.enterCallback = enterCallback
self.leaveCallback = leaveCallback
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.Type.Enter:
self.enterCallback(obj)
if event.type() == QtCore.QEvent.Type.Leave:
self.leaveCallback(obj)
return True
class TestClass:
def __init__(self):
self.app = QtGui.QApplication([])
# the scene with some rectangles
self.scene = QtGui.QGraphicsScene()
self.scene.addRect(30, 30, 100, 50, pen=QtGui.QPen(QtCore.Qt.darkGreen))
self.scene.addRect(150, 0, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkYellow))
self.scene.addRect(80, 80, 100, 20, pen=QtGui.QPen(QtCore.Qt.darkMagenta))
self.scene.addRect(200, 10, 30, 80, pen=QtGui.QPen(QtCore.Qt.darkRed))
self.window = QtGui.QWidget()
# put two graphicsviews into the window with different scaling for each
self.layout = QtGui.QVBoxLayout(self.window)
self.v1 = QtGui.QGraphicsView(self.scene)
self.v1.setFixedSize(500, 100)
self.v1.scale(0.5, 0.5)
self.v1.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.v1.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.layout.addWidget(self.v1)
self.v2 = QtGui.QGraphicsView(self.scene)
self.v2.setFixedSize(500, 500)
self.v2.scale(5, 5)
self.v2.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.v2.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.layout.addWidget(self.v2)
mouseFilter = MouseInsideFilterObj(self.enterGV, self.leaveGV)
self.v1.installEventFilter(mouseFilter)
# the tracker rectangle
self.tracker = MovableGraphicsRectItem(lambda pos: self.v2.setSceneRect(pos.x(), pos.y(), 100, 100))
self.tracker.setRect(0, 0, 100, 100)
self.v2.setSceneRect(0, 0, 100, 100)
self.tracker.setPen(QtGui.QPen(QtCore.Qt.darkCyan))
self.scene.addItem(self.tracker)
self.window.show()
self.app.exec_()
def leaveGV(self, gv):
if gv is self.v1:
self.tracker.deactivate()
def enterGV(self, gv):
if gv is self.v1:
self.tracker.activate()
TestClass()
I am just trying my first prototype in pyside (python/Qt). The application itself starts up fine, creates a window with widgets according to my layout. Threads are started and execute, all fine. Except...
I want to enhance the GUI by adding some custom widget indicating the execution state of the threads. So I thought flashing LEDs would be fine for that. For this I try to implement a custom LED widget.
Remember that I currently try to learn python, so there might be some strange approaches in this. Anyway, here is the LED widgets class in its current state:
from PySide import QtCore, QtGui
class LED(QtGui.QWidget):
class Mode:
STATIC_OFF = 0
STATIC_ON = 1
FLASH_SLOW = 2
FLASH_MEDIUM = 2
FLASH_FAST = 3
class Color:
BLACK = '#000000'
GREEN = '#00FF00'
RED = '#FF0000'
BLUE = '#0000FF'
YELLOW = '#FFFF00'
WHITE = '#FFFFFF'
mode = Mode.STATIC_ON
color = Color.BLACK
radius = 10
status = False
timer = None
outdated = QtCore.Signal()
def __init__(self, mode, color, radius, parent=None):
super(LED, self).__init__(parent)
self.outdated.connect(self.update)
self.setMode(mode,False)
self.setColor(color,False)
self.setRadius(radius,False)
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.adjustAppearance)
self.adjustAppearance()
def getCenter(self):
return QtCore.QPoint(self.radius, self.radius)
def getBox(self):
return QtCore.QRect(self.radius, self.radius)
def setColor(self, color, update=True):
assert color in (self.Color.GREEN,self.Color.RED,self.Color.BLUE,self.Color.YELLOW,self.Color.WHITE), "invalid color"
self.color = color
if update:
self.adjustAppearance()
def setMode(self, mode, update=True):
assert mode in (self.Mode.STATIC_OFF,self.Mode.STATIC_ON,self.Mode.FLASH_SLOW,self.Mode.FLASH_MEDIUM,self.Mode.FLASH_FAST),"invalid mode"
self.mode = mode
if update:
self.adjustAppearance()
def setRadius(self, radius, update=True):
assert isinstance(radius, int), "invalid radius type (integer)"
assert 10<=radius<=100, "invalid radius value (0-100)"
self.radius = radius
if update:
self.adjustAppearance()
def switchOn(self):
self.status = True
self.adjustAppearance()
def switchOff(self):
self.status = False
self.adjustAppearance()
def adjustAppearance(self):
if self.mode is self.Mode.STATIC_OFF:
self.status = False
self.timer.stop()
elif self.mode is self.Mode.STATIC_ON:
self.status = True
self.timer.stop()
elif self.mode is self.Mode.FLASH_SLOW:
self.status = not self.status
self.timer.start(200)
elif self.mode is self.Mode.FLASH_SLOW:
self.status = not self.status
self.timer.start(500)
elif self.mode is self.Mode.FLASH_SLOW:
self.status = not self.status
self.timer.start(1000)
self.outdated.emit()
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
self.drawWidget(event, painter)
painter.end()
def drawWidget(self, event, painter):
if self.status:
shade = QtGui.QColor(self.color).darker
else:
shade = QtGui.QColor(self.color).lighter
#painter.setPen(QtGui.QColor('black'), 1, QtCore.Qt.SolidLine)
painter.setPen(QtGui.QColor('black'))
painter.setBrush(QtCore.Qt.RadialGradientPattern)
painter.drawEllipse(self.getCenter(), self.radius, self.radius)
My problem is that the widget simply does not show when I add it to the windows layout. Other widgets (non-custome, plain Qt widgets) do show, so I gues it is not a question of creating the widget, not a question of how I use the widget. Nevertheless here is the (shortened) instanciation if the widget:
class View(QtGui.QMainWindow):
ui = None
def __init__(self, config, parent=None):
log.debug("window setup")
self.config = config
super(View, self).__init__(parent)
try:
self.ui = self.Ui(self)
self.setObjectName("appView")
self.setWindowTitle("AvaTalk")
self.show()
except RuntimeError as e:
log.error(e.message)
class Ui(QtCore.QObject):
# [...]
iconDetector = None
buttonDetector = None
# [...]
def __init__(self, window, parent=None):
log.debug("ui setup")
super(View.Ui, self).__init__(parent)
self.window = window
# central widget
log.debug("widget setup")
self.centralWidget = QtGui.QWidget()
self.widgetLayout = QtGui.QVBoxLayout(self.centralWidget)
# create toolbars
#self.createMenubar()
#self.createCanvas()
self.createToolbar()
#self.createStatusbar()
# visualize widget
self.window.setCentralWidget(self.centralWidget)
# actions
log.debug("actions setup")
self.actionQuit = QtGui.QAction(self.window)
self.actionQuit.setObjectName("actionQuit")
self.menuFile.addAction(self.actionQuit)
self.menubar.addAction(self.menuFile.menuAction())
log.debug("connections setup")
QtCore.QObject.connect(self.actionQuit, QtCore.SIGNAL("activated()"), self.window.close)
QtCore.QMetaObject.connectSlotsByName(self.window)
def createToolbar(self):
log.debug("toolbar setup")
self.toolbar = QtGui.QHBoxLayout()
self.toolbar.setObjectName("toolbar")
self.toolbar.addStretch(1)
# camera
# detector
self.iconDetector = LED(LED.Mode.STATIC_OFF,LED.Color.GREEN,10,self.window)
self.buttonDetector = IconButton("Detector", "detector",self.window)
self.toolbar.addWidget(self.iconDetector)
self.toolbar.addWidget(self.buttonDetector)
self.toolbar.addStretch(1)
# analyzer
# extractor
# layout
self.widgetLayout.addLayout(self.toolbar)
It might well be that the actual painting using QPainter is still nonsense. I did not yet come to test that: actually when testing I find that isVisible() returns False on the widget after the setup has completed. So I assume I miss a central point. Unfortunately I am unable to find out what I miss...
Maybe someone can spot my issue? Thanks !
One thing to be careful when implementing custom widgets derived from QWidget is: sizeHint or minimumSizeHint for QWidget returns invalid QSize by default. This means, if it is added to a layout, depending on the other widgets, it will shrink to 0. This effectively makes it 'not-visible'. Although, isVisible would still return True. Widget is 'visible', but it just doesn't have anything to show (0 size). So, if you're getting False, there is definitely another issue at hand.
So it is necessary to define these two methods with sensible sizes:
class LED(QtGui.QWidget):
# ...
def sizeHint(self):
size = 2 * self.radius + 2
return QtCore.QSize(size, size)
def minimumSizeHint(self):
size = 2 * self.radius + 2
return QtCore.QSize(size, size)
Note: There are other issues:
Like defining mode, color, etc as class attributes and then overriding them with instance attributes. They won't break anything but they are pointless.
painter.setBrush(QtCore.Qt.RadialGradientPattern) is wrong. You can't create a brush with QtCore.Qt.RadialGradientPattern. It is there, so that brush.style() can return something. If you want a gradient pattern you should create a brush with QGradient constructor.
if self.mode is self.Mode.STATIC_OFF: comparing with is is wrong. is compares identity, you want == here. (also, FLASH_SLOW and FLASH_MEDIUM are both 2)
assert is for debugging and unit tests. You shouldn't use it in real code. Raise an exception if you want. Or have sensible defaults, where invalid values would be replaced with that.