PyQt5: QGraphicsItem multiple inheritance - pyqt

I am overwriting an item that can be either rectangle or circle (share the same functions, properties). I'd like to have one class that is inherited from both shape as followed:
class Pad(QGraphicsRectItem, QGraphicsEllipseItem):
def __init__(self, pos, isCircle=False, parent=None):
w, h = 200, 100
if isCircle:
QGraphicsEllipseItem.__init__(self, -w/2, -h/2, w, h)
else:
QGraphicsRectItem.__init__(self, -w/2, -h/2, w, h)
This results with the only shape of the first parent (QGraphicsRectItem in this case). Could someone explain why and provide suggestion?
Full code below:
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Pad(QGraphicsRectItem, QGraphicsEllipseItem):
def __init__(self, pos, isCircle=False, parent=None):
w, h = 200, 100
if isCircle:
QGraphicsEllipseItem.__init__(self, -w/2, -h/2, w, h)
else:
QGraphicsRectItem.__init__(self, -w/2, -h/2, w, h)
self.setPos(pos)
self.parent = parent
self.color = QColor(255, 0, 0)
self.setPen(QPen(self.color, Qt.MiterJoin, 1))
self.setBrush(QBrush(self.color))
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
scene = QGraphicsScene()
rect = Pad(QPointF(0, 0), False, scene)
circle = Pad(QPointF(300, 300), True, scene)
scene.addItem(rect)
scene.addItem(circle)
view = QtWidgets.QGraphicsView(scene)
view.setRenderHints(QtGui.QPainter.Antialiasing)
view.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

TL;DR
Use a QGraphicsPathItem.
Explanation
The order of the base classes decides the final behavior of all methods: it's called method resolution order (MRO). The fact that you called the __init__ of a different class is completely irrelevant.
Consider the following:
class Rectangle:
def paint(self):
print('draw rectangle')
class Circle:
def paint(self):
print('draw circle')
class Test1(Rectangle, Circle):
def __init__(self):
Rectangle.__init__(self)
class Test2(Circle, Rectangle):
def __init__(self):
# note that we're still calling the __init__ of the Rectangle class
Rectangle.__init__(self)
>>> Test1().paint()
'draw rectangle'
>>> Test2().paint()
'draw circle'
This is because any override of any inherited base class will take precedence, based on the [reverse] order of inheritance: the method that will be actually called will be the first override in the MRO.
While, under certain aspects, the QGraphicsRectItem and QGraphicsEllipseItem classes are very similar, you must also always remember that PyQt (and PySide) are bindings: their behavior not only depends on the python class, but also on their wrapped C++ objects.
That's why it's usually impossible to use multiple inheritance of C++ derived classes[1].
If those classes were pure python, the solution would be quite easy: considering what explained above, the paint method should also call the method of the correct class:
class Pad(QGraphicsRectItem, QGraphicsEllipseItem):
def __init__(self, pos, isCircle=False, parent=None):
# ...
self.isCircle = isCircle
def paint(self, painter, option, widget=None):
if self.isCircle:
QGraphicsEllipseItem.paint(self, painter, option, widget)
else:
QGraphicsRectItem.paint(self, painter, option, widget)
Unfortunately, this is not the case for wrapped methods of bound C++ objects, as they internally access memory addresses for their properties, and you will most certainly get some unexpected behavior. If you're lucky, you'll only have strange graphical results. Worst case scenarios include the program freezing or even crash.
For instance, try adding the following line in the if isCircle: block of the init:
print(self.startAngle(), self.spanAngle())
Running the above gives unexpected (and unpredictable) results, like 97 -1220601744 or 105 -1220401040. Adding the same line in paint also gives a completely different result than what was printed in the init.
The solution is quite simple: don't try to use multiple inheritance, but use a better class as QGraphicsPathItem:
class Pad(QGraphicsPathItem):
def __init__(self, pos, isCircle=False, parent=None):
w, h = 200, 100
rect = QRectF(-w/2, -h/2, w, h)
path = QPainterPath()
if isCircle:
path.addEllipse(rect)
else:
path.addRect(rect)
super().__init__(path, parent)
self.setPos(pos)
self.parent = parent
self.color = QColor(255, 0, 0)
self.setPen(QPen(self.color, Qt.MiterJoin, 1))
self.setBrush(QBrush(self.color))
Note: the parent argument of a QGraphicsItem must always be a QGraphicsItem based class; if you intend to directly provide the scene for some reason, use a specific positional or keyworded argument (in your code you didn't use the parent argument in the class, but you provided it anyway, and it was of the wrong type); also consider that if you're trying to do that so that the item adds itself to the scene, that's generally not considered good practice, unless you really know what you're doing.

Related

Kivy: How to switch buttons by dragging over them

I have created an array of buttons in Kivy, and I need to switch their states (or change color/background) by touch-dragging over them. I can't figure out how to do it. The task is to create words by dragging over letter buttons. Should I use an invisible Scatter widget, or is there something dedicated for this purpose. Thank you.
Scatter would be a way, but it’s not necessary, you can implement on_touch_down, on_touch_move and on_touch_up methods of your widget class to handle being dragged and dropped other things, you’ll need to test collision of the dragged widget with possible landing zones (widgets) during drag and drop, to decide how to react to them, i have this example https://gist.github.com/tshirtman/7282822
from kivy.app import App
from kivy.garden.magnet import Magnet
from kivy.uix.image import Image
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from kivy.clock import Clock
from os import listdir
IMAGEDIR = '/usr/share/icons/hicolor/32x32/apps/'
IMAGES = filter(
lambda x: x.endswith('.png'),
listdir(IMAGEDIR))
kv = '''
FloatLayout:
BoxLayout:
GridLayout:
id: grid_layout
cols: int(self.width / 32)
FloatLayout:
id: float_layout
'''
class DraggableImage(Magnet):
img = ObjectProperty(None, allownone=True)
app = ObjectProperty(None)
def on_img(self, *args):
self.clear_widgets()
if self.img:
Clock.schedule_once(lambda *x: self.add_widget(self.img), 0)
def on_touch_down(self, touch, *args):
if self.collide_point(*touch.pos):
touch.grab(self)
self.remove_widget(self.img)
self.app.root.add_widget(self.img)
self.center = touch.pos
self.img.center = touch.pos
return True
return super(DraggableImage, self).on_touch_down(touch, *args)
def on_touch_move(self, touch, *args):
grid_layout = self.app.root.ids.grid_layout
float_layout = self.app.root.ids.float_layout
if touch.grab_current == self:
self.img.center = touch.pos
if grid_layout.collide_point(*touch.pos):
grid_layout.remove_widget(self)
float_layout.remove_widget(self)
for i, c in enumerate(grid_layout.children):
if c.collide_point(*touch.pos):
grid_layout.add_widget(self, i - 1)
break
else:
grid_layout.add_widget(self)
else:
if self.parent == grid_layout:
grid_layout.remove_widget(self)
float_layout.add_widget(self)
self.center = touch.pos
return super(DraggableImage, self).on_touch_move(touch, *args)
def on_touch_up(self, touch, *args):
if touch.grab_current == self:
self.app.root.remove_widget(self.img)
self.add_widget(self.img)
touch.ungrab(self)
return True
return super(DraggableImage, self).on_touch_up(touch, *args)
class DnDMagnet(App):
def build(self):
self.root = Builder.load_string(kv)
for i in IMAGES:
image = Image(source=IMAGEDIR + i, size=(32, 32),
size_hint=(None, None))
draggable = DraggableImage(img=image, app=self,
size_hint=(None, None),
size=(32, 32))
self.root.ids.grid_layout.add_widget(draggable)
return self.root
if __name__ == '__main__':
DnDMagnet().run()
(see comments on the gist for possible improvements though, i didn’t try them but they seem to make sense)
Which depends on the magnet widget (https://github.com/kivy-garden/garden.magnet) for nice effects, but this is not strictly necessary for you either, the important part is understanding the role of the on_touch_* methods and grabbing (grabbing makes sure a widget that started caring about a touch, gets all the updates for this touch, whatever other widgets think about it).
there is also a DragBehavior https://kivy.org/doc/stable/api-kivy.uix.behaviors.drag.html but i don’t see events in the documentation allowing to check collision on drop, that i think you need, but possibly subclassing this widget and implementing your changes in your subclass would be easier, as my example predates it, i didn’t try.

How to modify this PyQt5 current setup to enable drag resize between layouts

How to modify this current setup to enable resizing(horizontally and vertically) between the layouts shown below? Let's say I want to resize the lists in the right toward the left by dragging them using the mouse, I want the image to shrink and the lists to expand and same applies for in between the 2 lists.
Here's the code:
from PyQt5.QtWidgets import (QMainWindow, QApplication, QDesktopWidget, QHBoxLayout, QVBoxLayout, QWidget,
QLabel, QListWidget)
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt
import sys
class TestWindow(QMainWindow):
def __init__(self, left_ratio, right_ratio, window_title):
super().__init__()
self.left_ratio = left_ratio
self.right_ratio = right_ratio
self.current_image = None
self.window_title = window_title
self.setWindowTitle(self.window_title)
win_rectangle = self.frameGeometry()
center_point = QDesktopWidget().availableGeometry().center()
win_rectangle.moveCenter(center_point)
self.move(win_rectangle.topLeft())
self.tools = self.addToolBar('Tools')
self.left_widgets = {'Image': QLabel()}
self.right_widgets = {'List1t': QLabel('List1'), 'List1l': QListWidget(),
'List2t': QLabel('List2'), 'List2l': QListWidget()}
self.central_widget = QWidget(self)
self.main_layout = QHBoxLayout()
self.left_layout = QVBoxLayout()
self.right_layout = QVBoxLayout()
self.adjust_widgets()
self.adjust_layouts()
self.show()
def adjust_layouts(self):
self.main_layout.addLayout(self.left_layout, self.left_ratio)
self.main_layout.addLayout(self.right_layout, self.right_ratio)
self.central_widget.setLayout(self.main_layout)
self.setCentralWidget(self.central_widget)
def adjust_widgets(self):
self.left_layout.addWidget(self.left_widgets['Image'])
self.left_widgets['Image'].setPixmap(QPixmap('test.jpg').scaled(500, 400, Qt.IgnoreAspectRatio,
Qt.SmoothTransformation))
for widget in self.right_widgets.values():
self.right_layout.addWidget(widget)
if __name__ == '__main__':
test = QApplication(sys.argv)
test_window = TestWindow(6, 4, 'Test')
sys.exit(test.exec_())
One way to rescale the image to an arbitrary size while maintaining its aspect ratio is to subclass QWidget and override sizeHint and paintEvent and use that instead of a QLabel for displaying the image, e.g.
class PixmapWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._pixmap = None
def sizeHint(self):
if self._pixmap:
return self._pixmap.size()
else:
return QSize()
def setPixmap(self, pixmap):
self._pixmap = pixmap
self.update()
def paintEvent(self, event):
painter = QPainter(self)
super().paintEvent(event)
if self._pixmap:
size = self._pixmap.size().scaled(self.size(), Qt.KeepAspectRatio)
offset = (self.size() - size)/2
rect = QRect(offset.width(), offset.height(), size.width(), size.height())
painter.drawPixmap(rect, self._pixmap)
Since you are subclassing QMainWindow you could use DockWidgets to display the lists instead of adding them to the layout of the central widget, e.g.
class TestWindow(QMainWindow):
def __init__(self, left_ratio, right_ratio, window_title):
super().__init__()
#self.left_ratio = left_ratio <--- not needed since image and lists
#self.right_ratio = right_ratio <--- are not sharing a layout anymore
...
# use PixmapWidget instead of QLabel for showing image
# refactor dictionary for storing lists to make adding DockWidgets easier
self.left_widgets = {'Image': PixmapWidget()}
self.right_widgets = {'List1': QListWidget(),
'List2': QListWidget()}
self.central_widget = QWidget(self)
# self.main_layout = QHBoxLayout() <-- not needed anymore
self.left_layout = QVBoxLayout()
self.adjust_widgets()
self.adjust_layouts()
self.show()
def adjust_layouts(self):
self.central_widget.setLayout(self.left_layout)
self.setCentralWidget(self.central_widget)
def adjust_widgets(self):
self.left_layout.addWidget(self.left_widgets['Image'])
self.left_widgets['Image'].setPixmap(QPixmap('test.jpg').scaled(500, 400, Qt.IgnoreAspectRatio, Qt.SmoothTransformation))
self.dock_widgets = []
for text, widget in self.right_widgets.items():
dock_widget = QDockWidget(text)
dock_widget.setFeatures(QDockWidget.NoDockWidgetFeatures)
dock_widget.setWidget(widget)
self.addDockWidget(Qt.RightDockWidgetArea, dock_widget)
self.dock_widgets.append(dock_widget)
Screenshots
You need to use QSplitter.
It acts almost like a box layout, but has handles that allow the resizing of each item.
Be aware that you can only add widgets to a QSplitter, not layouts, so if you need to add a "section" (a label and a widget) that can resize its contents, you'll have to create a container widget with its own layout.
Also note that using dictionaries for these kind of things is highly discouraged. For versions of Python older than 3.7, dictionary order is completely arbitrary, and while sometimes it might be consistent (for example, when keys are integers), it usually isn't: with your code some times the labels were put all together, sometimes the widgets were inverted, etc., so if somebody is using your program with <=3.6 your interface won't be consistent. Consider that, while python 3.6 will reach end of life in 2022, it's possible that even after that a lot of people will still be using previous versions.
If you need a way to group objects, it's better to use a list or a tuple, as I did in the following example.
If you really "need" to use a key based group, then you can use OrderedDict, but it's most likely that there's just something wrong with the logic behind that need to begin with.
class TestWindow(QMainWindow):
def __init__(self, left_ratio, right_ratio, window_title):
super().__init__()
self.left_ratio = left_ratio
self.right_ratio = right_ratio
self.current_image = None
self.window_title = window_title
self.setWindowTitle(self.window_title)
win_rectangle = self.frameGeometry()
center_point = QDesktopWidget().availableGeometry().center()
win_rectangle.moveCenter(center_point)
self.move(win_rectangle.topLeft())
self.tools = self.addToolBar('Tools')
self.left_widgets = {'Image': QLabel()}
self.right_widgets = [(QLabel('List1'), QListWidget()),
(QLabel('List2'), QListWidget())]
self.central_widget = QSplitter(Qt.Horizontal, self)
self.setCentralWidget(self.central_widget)
self.right_splitter = QSplitter(Qt.Vertical, self)
self.adjust_widgets()
self.central_widget.setStretchFactor(0, left_ratio)
self.central_widget.setStretchFactor(1, right_ratio)
self.show()
def adjust_widgets(self):
self.central_widget.addWidget(self.left_widgets['Image'])
self.left_widgets['Image'].setPixmap(QPixmap('test.jpg').scaled(500, 400, Qt.IgnoreAspectRatio,
Qt.SmoothTransformation))
self.left_widgets['Image'].setScaledContents(True)
self.central_widget.addWidget(self.right_splitter)
for label, widget in self.right_widgets:
container = QWidget()
layout = QVBoxLayout(container)
layout.addWidget(label)
layout.addWidget(widget)
self.right_splitter.addWidget(container)

Where should functions that control the window attributes go in a MCV design pattern for Tkinter?

I'm designing a Tkinter app and I really want to apply proper structure to my application. One thing I still struggle with is where certain methods should go.
In this case, I have a function that centers the main window once the GUI runs so that the user sees the window in the middle of their screen. I'm thinking that this function should be a method in the Controller class but I also thought it could go in the Model class because it's technically logic? Or maybe it shouldn't be in a class at all?
I don't want to get to broad, but is there generally a good way to think about where too place methods? Like a checklist of sorts to go through when making design assumptions regarding the methods put into a class?
CODE
import tkinter as tk
class Model:
def __init__(self):
pass
class View:
def __init__(self, master):
self.master_frame = tk.Frame(master)
class Controller:
def __init__(self):
self.root = tk.Tk()
self.model = Model()
self.view = View(self.root)
def run(self):
self.root.title("My App")
self.middle_of_screen(1350, 750)
self.root.resizable(False, False)
self.root.iconbitmap('images/an_image.ico')
self.root.mainloop()
# Not exactly sure where it's most conceptually correct to put this method
def middle_of_screen(self, window_width, window_height):
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)
self.root.geometry(f'{window_width}x{window_height}+{x}+{y}')
if __name__ == '__main__':
c = Controller()
c.run()
Unless you want to parametrize where the window goes, its geometry and positioning belongs to the GUI.
The controller could be used to manage interaction with the user, in case you wanted to make the positioning an actionable property; however, the controller should not even be aware that the GUI uses tkinter or some other framework...
In a more elaborate app, the controller could be in charge of loading an ini file that is passed to the GUI.
The positioning, geometry, or anything related to the view has nothing to do with the model in any case. The model ignores both the controller and the GUI.
In your case, I would suggest something like this:
import tkinter as tk
class Model:
def __init__(self):
pass
class Controller:
def __init__(self):
self.model = Model()
self.view = View()
self.view.start()
class View(tk.Tk):
def __init__(self):
super().__init__()
self.master_frame = tk.Frame(self)
self.title("My App")
self.middle_of_screen()
self.resizable(False, False)
self.iconbitmap('images/an_image.ico')
def middle_of_screen(self):
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)
self.geometry(f'{window_width}x{window_height}+{x}+{y}')
def start(self):
self.mainloop()

Why doesn't this custom QWidget display correctly

I'm retrying this question with a much better code example.
The code below, in its current form, will display a green shaded QWidget in a window, which is what I want. However, when commenting out the line:
self.widget = QWidget(self.centralwidget)
and uncommenting,
self.widget = Widget_1(self.centralwidget)
the green box doesn't display. The Widget_1 class is a simple subclass of QWidget, so I'm trying to wrap my head around where the breakdown is occurring. There are no error messages, and the print("Test") line within the Widget_1 class is outputting just fine, so I know everything is being called properly.
I'm not looking to use any type of automated layouts for reasons I don't need to go into here. Can you help me to understand why the green rectangle isn't displaying, and what correction I would need to make in order to utilize the Widget_1 class?
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtCore import QRect
import sys
class Main_Window(object):
def setupUi(self, seating_main_window):
seating_main_window.setObjectName("seating_main_window")
seating_main_window.setEnabled(True)
seating_main_window.resize(400, 400)
self.centralwidget = QWidget(seating_main_window)
self.centralwidget.setObjectName("centralwidget")
########### The following two lines of code are causing the confusion #######
# The following line, when uncommented, creates a shaded green box in a window
self.widget = QWidget(self.centralwidget) # Working line
# The next line does NOT create the same shaded green box. Where is it breaking?
# self.widget = Widget_1(self.centralwidget) # Non-working line
self.widget.setGeometry(QRect(15, 150, 60, 75))
self.widget.setAutoFillBackground(False)
self.widget.setStyleSheet("background: rgb(170, 255, 0)")
self.widget.setObjectName("Widget1")
seating_main_window.setCentralWidget(self.centralwidget)
class Widget_1(QWidget):
def __init__(self, parent=None):
super().__init__()
self.setMinimumSize(10, 30) # I put this in thinking maybe I just couldn't see it
print("Test") # I see this output when run when Widget_1 is used above
class DemoApp(QMainWindow, Main_Window):
def __init__(self):
super().__init__()
self.setupUi(self)
if __name__ == '__main__': # if we're running file directly and not importing it
app = QApplication(sys.argv) # A new instance of QApplication
form = DemoApp() # We set the form to be our ExampleApp (design)
form.show() # Show the form
app.exec_() # run the main function
Accoriding to this Qt Wiki article:
How to Change the Background Color of QWidget
you must implement paintEvent in a custom QWidget subclass in order to use stylesheets. Also, since the widget is not part of a layout, you must give it a parent, otherwise it will not be shown. So your Widget_1 class must look like this:
from PyQt5.QtWidgets import QStyleOption, QStyle
from PyQt5.QtGui import QPainter
class Widget_1(QWidget):
def __init__(self, parent=None):
super().__init__(parent) # set the parent
print("Test")
def paintEvent(self, event):
option = QStyleOption()
option.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(QStyle.PE_Widget, option, painter, self)
super().paintEvent(event)

PYQT QSplitter issue

I use QSplitter and I found out that the minumum width of a widget in
the splitter is 32 pixels (and 23 pixels in height). Does anybody body knows how
to change this default. In other words, you can't drag the splitter so that one of the
widgets (assume that there are 2 widgets in the spllitter) in the spllitter will be less
than 32 pixels in width.
The code:
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.initUI()
def initUI(self):
self.resize(400,400)
m = QtGui.QSplitter(self)
m.resize(200, 100)
x = QtGui.QPushButton(m)
x.setGeometry(0, 0, 100, 100)
y = QtGui.QPushButton(m)
y.setGeometry(0, 100, 100, 100)
m.setSizes([20, 180])
# this will show you that the width of x is 32 (it should be 20!)
print x.width()
Note: I'm using Python 3.6.2 and PyQt5, though the logic in the example stays the same and can be understood even if you're using other versions of Python and PyQt.
Look at what is said here:
If you specify a size of 0, the widget will be invisible. The size policies of the widgets are preserved. That is, a value smaller than the minimal size hint of the respective widget will be replaced by the value of the hint.
One of the options to solve your problem is to call x.setMinimumWidth() with a small value, like:
x.setMinimumWidth(1)
However, if you'll try it yourself, you'll see that
it is a dirty hack as it actually leaves the widget here, just makes it very narrow and
though now you can drag the splitter, the initial width of the widget is still "32" instead of "20".
x.setMinimumWidth(0)
also doesn't work as expected: its minimal width is actually zero by default (as this widget has no contents, I guess), but it doesn't help you to make splitter item less than 32 pixels wide unless you collapse it.
By the way, set
m.setCollapsible(0, False)
m.setCollapsible(1, False)
if you want splitter to stop collapsing its two children widgets. More details here.
The solution I've found is to overload sizeHint() method of the widget you want to include into the splitter, as in example below (look at the ButtonWrapper class and what is output like now).
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#Python 3.6.2 and PyQt5 are used in this example
from PyQt5.QtWidgets import (
QPushButton,
QSplitter,
QWidget,
QApplication,
)
import sys
class ButtonWrapper(QPushButton):
def sizeHint(self):
return self.minimumSize()
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.resize(400, 400)
m = QSplitter(self)
m.resize(200, 100)
x = ButtonWrapper(self)
x.setGeometry(0, 0, 100, 100)
y = QPushButton(self)
y.setGeometry(0, 100, 100, 100)
m.addWidget(x)
m.addWidget(y)
m.setSizes([20, 180])
#Now it really shows "20" as expected
print(x.width())
#minimumWidth() is zero by default for empty QPushButton
print(x.minimumWidth())
#Result of our overloaded sizeHint() method
print(x.sizeHint().width())
print(x.minimumSizeHint().width())
self.setWindowTitle('Example')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
I'm not sure if this is the right way to do stuff, but I've spent lots of time trying to solve my own problem connected to this, and haven't seen anything satisfying yet so far. I'll really appreciate it if someone knows a better actually working & clear workaround.

Resources