Kivy: How to switch buttons by dragging over them - python-3.x

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.

Related

What causes a nested QRubberband to move unexpectedly?

I am just curious if I can make a nested QRubberband. (I or someone might find a use to it). I managed to edit the code from this answer to make a nested QRubberband. It is all fine and working until I move the QRubberband inside its parent QRubberband. I was very confused as it moves wildly when I'm dragging it.
This is the sample code:
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ResizableRubberBand(QRubberBand):
moving = False
def __init__(self, parent=None):
super(ResizableRubberBand, self).__init__(QRubberBand.Rectangle, parent)
self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.draggable = True
self.dragging = False
self.is_dragging = False
self.dragging_threshold = 5
self.mousePressPos = None
self.borderRadius = 5
self.setWindowFlags(Qt.SubWindow)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignLeft | Qt.AlignTop)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignRight | Qt.AlignBottom)
self.show()
def resizeEvent(self, event):
self.clearMask()
def paintEvent(self, event):
super().paintEvent(event)
qp = QPainter(self)
qp.setRenderHint(QPainter.Antialiasing)
qp.translate(.5, .5)
qp.drawRoundedRect(self.rect().adjusted(0, 0, -1, -1),
self.borderRadius, self.borderRadius)
def mousePressEvent(self, event):
if self.draggable and event.button() == Qt.RightButton:
self.mousePressPos = event.pos()
if event.button() == Qt.LeftButton:
self.first_mouse_location = (event.x(), event.y())
self.band = ResizableRubberBand(self)
self.band.setGeometry(event.x(), event.y(), 0, 0)
super(ResizableRubberBand, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.draggable and event.buttons() & Qt.RightButton:
diff = event.pos() - self.mousePressPos
if not self.dragging:
if diff.manhattanLength() > self.dragging_threshold:
self.dragging = True
if self.dragging:
geo = self.geometry()
parentRect = self.parent().rect()
geo.translate(diff)
if not parentRect.contains(geo):
if geo.right() > parentRect.right():
geo.moveRight(parentRect.right())
elif geo.x() < parentRect.x():
geo.moveLeft(parentRect.x())
if geo.bottom() > parentRect.bottom():
geo.moveBottom(parentRect.bottom())
elif geo.y() < parentRect.y():
geo.moveTop(parentRect.y())
self.move(geo.topLeft())
if event.buttons() & Qt.LeftButton:
first_mouse_location_x = self.first_mouse_location[0]
first_mouse_location_y = self.first_mouse_location[1]
new_x, new_y = event.x(), event.y()
difference_x = new_x - first_mouse_location_x
difference_y = new_y - first_mouse_location_y
self.band.resize(difference_x, difference_y)
super(ResizableRubberBand, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.mousePressPos is not None:
if event.button() == Qt.RightButton and self.dragging:
event.ignore()
self.dragging = False
self.mousePressPos = None
super(ResizableRubberBand, self).mouseReleaseEvent(event)
class mQLabel(QLabel):
def __init__(self, parent=None):
QLabel.__init__(self, parent)
self.setContentsMargins(0,0,0,0)
self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.first_mouse_location = (event.x(), event.y())
self.band = ResizableRubberBand(self)
self.band.setGeometry(event.x(), event.y(), 0, 0)
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
first_mouse_location_x = self.first_mouse_location[0]
first_mouse_location_y = self.first_mouse_location[1]
new_x, new_y = event.x(), event.y()
difference_x = new_x - first_mouse_location_x
difference_y = new_y - first_mouse_location_y
self.band.resize(difference_x, difference_y)
class App(QWidget):
def __init__(self):
super().__init__()
## Set main window attributes
self.setFixedSize(1000,600)
# Add Label
self.label = mQLabel()
self.label.setStyleSheet("border: 1px solid black;")
self.label_layout = QHBoxLayout(self)
self.label_layout.addWidget(self.label)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I'm trying to figure it out for 2 hours but I can't really seem to figure out what causes the unnecessary movement. My best guess is it is coming from the mouseMoveEvent but I'm not quite sure if it is from the parent QRubberband or from the QRubberband inside. I hope someone can figure out what is happening here.
The problem is the call to the base implementation of mouse events, which by default gets propagated to the parent for widgets that do not directly implement them, including QRubberBand, which normally doesn't intercept mouse events at all (which we restored by disabling the relative window attribute).
Since the parent itself is a rubber band, it will be moved itself too, making the movement recursive for the child, since it receives a mouse move exactly due to the fact that its been moved: remember that if a widget is moved and the mouse doesn't directly follow the same movement, it will potentially receive a mouse move event relative to its new position.
You can either return before calling it when you handle it, or not call it at all, depending on your needs.
The important thing is that it's consistent (especially for press and move), otherwise a widget could receive a mouse move without receiving the mouse press, which will crash as the variables have not been set yet.
Be aware that if you're in the process of making a more advanced editor for clipping/selections, drawing, etc, you should really consider using the Graphics View Framework: while much more complex and with a more steep learning curve, you'll soon find out that continuing development on basic QWidgets becomes gradually much more convoluted and difficult, to a point where it is really hard to fix things, especially if you're going to deal with image scaling or even basic scroll and zoom.
QWidget and QLabel implementations are not intended for image management, not even simple editing, and custom placed/painted/nested widgets are often difficult to deal with. Consider that doing a similar selection tool would have been much more easy in a graphics scene: for instance, the moving implementation would be almost completely unnecessary, as it's enough to set a simple flag to make an item movable.

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)

How to work with a isometric / orthogonal view in Kivy?

The image below is a GridLayout 10 x 10 with buttons.
I'd like to create the same Grid but in an isometric / orthogonal 2d view.
It means that every button, instead of being a square, it might be like a Rhombus, as the image below:
How can I do this?
I don't think you can actually do a 3D rotation on kivy UIX widgets, but you can do 2D rotations, and scaling. Here is an example of an App that does it in the build() method:
from kivy.app import App
from kivy.graphics.context_instructions import PushMatrix, Rotate, Scale, PopMatrix
from kivy.properties import BooleanProperty
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
import numpy as np
def matrixToNumpy(mat):
a = []
for i in range(4):
b = []
for j in range(4):
b.append(mat[i*4+j])
a.append(b)
npmat = np.mat(a)
return npmat
class MyButton(Button):
def on_touch_down(self, touch):
if not self.parent.touched:
self.parent.touched = True
if self.parent.mat is None:
scale = matrixToNumpy(self.parent.sca.matrix)
rotate = matrixToNumpy(self.parent.rot.matrix)
self.parent.mat = np.matmul(rotate, scale)
self.parent.inv_mat = self.parent.mat.I
npTouch = np.mat([touch.x, touch.y, 0, 1.0])
convTouch = np.matmul(npTouch, self.parent.inv_mat)
touch.x = convTouch[0,0]
touch.y = convTouch[0,1]
return super(MyButton, self).on_touch_down(touch)
def on_touch_up(self, touch):
self.parent.touched = False
return super(MyButton, self).on_touch_up(touch)
class MyGridLayout(GridLayout):
touched = BooleanProperty(False)
def __init__(self, **kwargs):
super(MyGridLayout, self).__init__(**kwargs)
self.mat = None
self.inv_mat = None
class MyApp(App):
def build(self):
layout = MyGridLayout(cols=10)
with layout.canvas.before:
PushMatrix()
layout.sca = Scale(1.0, 0.5, 1.0)
layout.rot = Rotate(angle=45, axis=(0,0,1), origin=(400,300,0))
with layout.canvas.after:
PopMatrix()
for i in range (1, 101):
layout.add_widget(MyButton(text=str(i)))
return layout
MyApp().run()
I suspect that with clever application of those two kivy.graphics.context_instructions, you can simulate what you want. The PushMatrix() and PopMatrix() confine the effects of the Scale and Rotate to just the MyGridLayout. You should be able to adjust these values using the layout.sca and layout.rot references.
I noticed after my original answer that the Buttons looked good, but no longer worked. I added a bunch of code to address that issue. All the numpy matrix stuff is just to get the mouse press position into the same coordinates as the MyGridLayout. Unfortunately, applying Canvas scaling and rotations isn't automatically taken into account by the Kivy events, so the additional code is necessary.
Here is what it looks like:

How to prevent widget's size changing in Kivy?

I've been playing with this code and I've found some (probably) strange thing: when I add parent to my class, size changes to [100, 100] (see comments):
from random import random, randint
import kivy
kivy.require('1.8.0')
from kivy.config import Config
Config.set('graphics', 'fullscreen', '0')
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.graphics import Color, Line, Ellipse, Triangle, Rectangle
class MyPaintWidget(Widget):
def on_touch_down(self, touch):
with self.canvas:
Color(random(), 1, 1, mode='hsv')
touch.ud['line'] = Line(points=(self.width - touch.x, self.height - touch.y))
print(self.width, self.height) # It works OK if I return painter below, but it's not if I return parent.
def on_touch_move(self, touch):
touch.ud['line'].points += [self.width - touch.x, self.height - touch.y]
class Example(App):
def build(self):
parent = Widget()
painter = MyPaintWidget()
print(painter.size) # Shows [100, 100] anyway.
parent.add_widget(painter)
return parent # If I return painter, everything works as I want (except painter.size still shows [100, 100]).
if __name__ == '__main__':
Example().run()
Why does that happen? And how should I make it right?
(100, 100) is the default size for widgets. In this case your painter has that size because you never set it to anything else.
Even if its parent were a layout class that would automatically move and resize the painter, its size would still read (100, 100) at this point because the layout hasn't had time to run yet. You shouldn't generally worry about the pixel values at this point - if you need something else to depend on them, use a binding to update the other thing automatically when the first one changes. This is made particularly easy by kv language.

PyQt4 how do I find the row number of a Widget in a QGridLayout?

I have a PyQt4 application with a QGridLayout as Layout. This layout has n widgets in it, each on another row, but not on another column. I have made all the widgets using a constructor. I was wondering, how do I get the row number of a widget in the grid layout, so that when I click on it, it gets that number and I can use it further in my code.
The code looks like this:
...
class sampleWidget(QWidget):
def __init__(self):
QWidget.__init__(self)
...
self.show()
....
class mainClass(QWidget):
def __init__(self):
QWidget.__init__(self)
layout = QGridLayout()
self.setLayout(layout)
for i in xrange(10):
widget = sampleWidget()
widget.setObjectName("samplewidget" + i)
layout.addWidget(i, 0)
self.show()
....
I have made all the necessary imports and all what is needed to run the program, don't worry. My only worry is how to get the row number of a widget created.
If someone is willing to help me I would be very grateful!
Have a great day.
I might be missing something obvious but this is at least one way of doing it.
Edit: I wasn't happy with my first suggestion. Changed it therefore. Might be a bit overboard in regards to the question but should show how to get the information you asked for.
from PyQt4 import QtGui, QtCore
import sys, collections
pos = collections.namedtuple("pos", ("row, column"))
class Widget(QtGui.QWidget):
itemSelected = QtCore.pyqtSignal(QtGui.QWidget, pos)
def __init__(self):
super(Widget, self).__init__()
layout = QtGui.QGridLayout(self)
for y in range(0, 11):
layout.addWidget(QtGui.QLabel("Row: %d" % y, self), y, 0)
for x in range(1,4):
layout.addWidget(QtGui.QLabel("QLabel"), y, x)
self.itemSelected.connect(self.onItemSelected)
def mousePressEvent(self, event):
widget = self.childAt(event.pos())
if isinstance(widget, QtGui.QLabel): # Or whatever type you are looking for
self._handleEvent(widget)
return QtGui.QWidget.mousePressEvent(self, event)
def _handleEvent(self, widget):
layout = self.layout()
index = layout.indexOf(widget)
row, column, cols, rows = layout.getItemPosition(index)
self.itemSelected.emit(widget, pos(row, column))
def onItemSelected(self, widget, pos):
print "%s at %s" % (widget, pos)
if __name__ == "__main__":
app = QtGui.QApplication([])
wnd = Widget()
wnd.show()
sys.exit(app.exec_())

Resources