I have a simple app in which i would like to insert a scroll view of several buttons.
So there is the base code, i want a scroll view inside the grid layout.
PS: i have this error: Menu object has no attribute 'view'
what I would like to obtain:
debug.py:
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder
from kivy.uix.button import Button
class AppScreenManager(ScreenManager):
def __init__(self, **kwargs):
super(AppScreenManager, self).__init__(**kwargs)
class Menu(Screen):
def __init__(self, **kwargs):
super(Menu, self).__init__(**kwargs)
base = ["element {}".format(i) for i in range(40)]
for element in base:
self.view.add_widget(Button(text=element, size=(40,40), size_hint=(1, None), background_color=(0.5, 0.5, 0.5, 1), color=(1,1,1,1)))
Builder.load_file("debug.kv")
class MyAppli(App):
def build(self):
return AppScreenManager()
if __name__ == '__main__':
MyAppli().run()
debug.kv:
#:kivy 1.9.1
<AppScreenManager>:
Menu:
<Menu>:
BoxLayout:
orientation: 'vertical'
BoxLayout:
size: (64, 64)
size_hint: (1, None)
Button:
text: "Menu"
color: (1,1,1,1)
background_color: (.3, .3, .3, 1)
GridLayout: # here i want a scrollview
id: view
cols: 1
Kivy Language
Note that the outermost widget applies the kv rules to all its inner
widgets before any other rules are applied. This means if an inner
widget contains ids, these ids may not be available during the inner
widget’s init function.
ScrollView » Managing the Content Size and Position
By default, the size_hint is (1, 1), so the content size will fit your ScrollView exactly (you will have nothing to scroll). You must deactivate at least one of the size_hint instructions (x or y) of the child to enable scrolling. Setting size_hint_min to not be None will also enable scrolling for that dimension when the ScrollView is smaller than the minimum size.
To scroll a GridLayout on it’s Y-axis/vertically, set the child’s
width to that of the ScrollView (size_hint_x=1), and set the
size_hint_y property to None:
Use Clock.schedule_once to invoke a new method, create_scrollview. Make sure the height is such that there is something to scroll layout.bind(minimum_height=layout.setter('height')). Please refer to the example below for details.
Example
debug.py
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window
from kivy.properties import ObjectProperty
from kivy.clock import Clock
class AppScreenManager(ScreenManager):
def __init__(self, **kwargs):
super(AppScreenManager, self).__init__(**kwargs)
class Menu(Screen):
view = ObjectProperty(None)
def __init__(self, **kwargs):
super(Menu, self).__init__(**kwargs)
Clock.schedule_once(self.create_scrollview)
def create_scrollview(self, dt):
base = ["element {}".format(i) for i in range(40)]
layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
layout.bind(minimum_height=layout.setter("height"))
for element in base:
layout.add_widget(Button(text=element, size=(50, 50), size_hint=(1, None),
background_color=(0.5, 0.5, 0.5, 1), color=(1, 1, 1, 1)))
scrollview = ScrollView(size_hint=(1, None), size=(Window.width, Window.height))
scrollview.add_widget(layout)
self.view.add_widget(scrollview)
Builder.load_file("debug.kv")
class MyAppli(App):
def build(self):
return AppScreenManager()
if __name__ == '__main__':
MyAppli().run()
debug.kv
#:kivy 1.10.0
<AppScreenManager>:
Menu:
<Menu>:
view: view
BoxLayout:
orientation: 'vertical'
BoxLayout:
size: (64, 64)
size_hint: (1, None)
Button:
text: "Menu"
color: (1, 1, 1, 1)
background_color: (.3, .3, .3, 1)
ScrollView:
id: view
Output
ScrollView:
size: self.size
GridLayout: # here i want a scrollview
id: view
cols: 1
size_hint_y: None
height: self.minimum_height
Button:
text:
size_hint_y: None
Do something like this and add the buttons needed in the gridlayout... I'm new to Kivy so please bear with me.
Related
How to change the following code to get the QLabel stretch to width of the window ?
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 200, 100)
self.label = QLabel('Hello World!', self)
self.label.setAlignment(Qt.AlignCenter)
self.label.setStyleSheet('font-size: 12pt; background-color: red')
self.show()
app = QApplication([])
win = Window()
app.exec()
As the documentation about QMainWindow says, you must set a central widget for it:
Creating a main window without a central widget is not supported. You must have a central widget even if it is just a placeholder.
The problem is that you need a layout manager in order to properly adapt widget sizes inside a parent, and just manually setting widget geometries is generally discouraged.
You created the label as a direct child, so it will have no knowledge about its parents size changes.
Just set the label as central widget.
self.setCentralWidget(self.label)
Otherwise, you can use a container widget, set a layout and add the label to it, but you still must set the central widget.
central = QWidget()
layout = QVBoxLayout(central)
layout.addWidget(self.label)
self.setCentralWidget(central)
The alternative is to directly use a QWidget instead of QMainWindow as you did in your answer.
you can use sizePolicy
self.label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
Thank you very much for your answers. The problem is now solved by the following code changes
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Window(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 200, 100)
self.label = QLabel('Hello World!', self)
self.label.setAlignment(Qt.AlignCenter)
self.label.setStyleSheet('font-size: 12pt; background-color: red')
self.box_layout = QHBoxLayout()
self.box_layout.addWidget(self.label)
self.setLayout(self.box_layout)
self.show()
app = QApplication([])
win = Window()
app.exec()
Edit: laytout -> box_layout
I have created a KivyMD Menu. When I click on the main Button, the menu is opening without a problem. However when I click on a menu button the value of the main button is not changing. Nothing happens. I thought the code is sufficient to achieve it. Does anyone know a solution? Thank you in advance!
py file:
from kivy.core.window import Window
from kivy.uix.screenmanager import Screen
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
Window.size = (400, 800)
class homescreen(Screen):
pass
GUI = Builder.load_file("main.kv")
class MainApp(MDApp, homescreen):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.homescreen = Builder.load_string("homescreen")
menu_items = [{"icon": "git", "text": f"Item {i}"} for i in range(5)]
self.menu = MDDropdownMenu(
caller=self.ids.drop_item,
items=menu_items,
#position="center",
width_mult=4,
)
self.menu.bind(on_release=self.set_item)
def set_item(self, instance_menu, instance_menu_item):
self.ids.drop_item.set_item(instance_menu_item.text)
self.menu.dismiss()
def build(self):
return self.homescreen
if __name__ == "__main__":
MainApp().run()
main.kv:
<homescreen>:
MDDropDownItem:
id: drop_item
pos_hint: {'center_x': .5, 'center_y': .5}
text: "Select"
on_release: app.menu.open()
Here is your improved code
py file:
from kivy.core.window import Window
from kivy.uix.screenmanager import Screen
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
Window.size = (400, 800)
class homescreen(Screen):
pass
GUI = Builder.load_file("main.kv")
class MainApp(homescreen):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.homescreen = Builder.load_string("homescreen")
menu_items = [{"icon": "git", "text": f"Item {i}"} for i in range(5)]
self.menu = MDDropdownMenu(
caller=self.ids.drop_item,
items=menu_items,
callback=self.set_item,
width_mult=4,
)
def set_item(self, instance_menu_item):
self.ids.drop_item.text = instance_menu_item.text
self.menu.dismiss()
def build(self):
return self.homescreen
class app(MDApp):
def build(self):
return MainApp()
if __name__ == "__main__":
app().run()
main.ky
<homescreen>:
MDDropDownItem:
id: drop_item
pos_hint: {'center_x': .5, 'center_y': .5}
text: "Select"
on_release: root.menu.open()
I need to move a QGraphicsPixmapItem through a circle that it is at the top left corner of the image. That is, when I grab with the mouse the circle, I need the top left corner of the image to follow the circle. I subclassed a QGraphicsEllipseItem and reimplemented the itemChange method but when I set the position of the image to that value, the image is not being positioned correctly. What should I modify in my code?
import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsView
from PyQt5 import QtGui, QtWidgets
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.scene = Scene()
self.view = QGraphicsView(self)
self.setGeometry(10, 30, 850, 600)
self.view.setGeometry(20, 22, 800, 550)
self.view.setScene(self.scene)
class Scene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(Scene, self).__init__(parent)
# other stuff here
self.set_image()
def set_image(self):
image = Image()
self.addItem(image)
image.set_pixmap()
class Image(QtWidgets.QGraphicsPixmapItem):
def __init__(self, parent=None):
super(Image, self).__init__(parent)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
def set_pixmap(self):
pixmap = QtGui.QPixmap("image.jpg")
self.setPixmap(pixmap)
self.pixmap_controller = PixmapController(self)
self.pixmap_controller.set_pixmap_controller()
self.pixmap_controller.setPos(self.boundingRect().topLeft())
self.pixmap_controller.setFlag(QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges, True)
def change_image_position(self, position):
self.setPos(position)
class PixmapController(QtWidgets.QGraphicsEllipseItem):
def __init__(self, pixmap):
super(PixmapController, self).__init__(parent=pixmap)
self.pixmap = pixmap
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
color = QtGui.QColor(0, 0, 0)
brush = QtGui.QBrush(color)
self.setBrush(brush)
def set_pixmap_controller(self):
self.setRect(-5, -5, 10, 10)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange:
self.pixmap.change_image_position(value)
return super(PixmapController, self).itemChange(change, value)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
When a graphics item has a parent, its coordinate system is based on that parent, not on the scene.
The problem is that when you try to move the PixmapController, the movement is in parent coordinates (the pixmap item). When you check for the ItemPositionChange you are you're changing the parent position but the item position is changed anyway, based on the parent coordinate system.
While you could just return an empty QPoint (which will not change the item position), this wouldn't be a good choice: as soon as you release the mouse and start to move it again, the pixmap will reset its position.
The solution is not to set the movable item flag, but filter for mouse movements, compute a delta based on the click starting position, and use that delta to move the parent item based on its current position.
class PixmapController(QtWidgets.QGraphicsEllipseItem):
def __init__(self, pixmap):
super(PixmapController, self).__init__(parent=pixmap)
self.pixmap = pixmap
# the item should *NOT* move
# self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
color = QtGui.QColor(0, 0, 0)
brush = QtGui.QBrush(color)
self.setBrush(brush)
def set_pixmap_controller(self):
self.setRect(-5, -5, 10, 10)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.startPos = event.pos()
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
delta = event.pos() - self.startPos
self.parentItem().setPos(self.parentItem().pos() + delta)
If you want to use your change_image_position function, you need to change those functions accordingly; the code below does the same thing as the last line in the example above:
class Image(QtWidgets.QGraphicsPixmapItem):
# ...
def change_image_position(self, delta):
self.setPos(self.pos() + delta)
class PixmapController(QtWidgets.QGraphicsEllipseItem):
# ...
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
delta = event.pos() - self.startPos
self.pixmap.change_image_position(delta)
Tip: do not add a child widget to a QMainWindow like that, as it will not resize correctly when the window is resized. Use self.setCentralWidget(self.view) instead; if you want to add margins, use a container QWidget, set that widget as the central widget, add a simple QHBoxLayout (or QVBoxLayout), add the view to that layout and then set the margins with layout.setContentsMargins(left, top, right, bottom)
Trying to figure out how to implement a straight-forward ScrollView in KV language based on the examples given in the documentation. I cannot believe I cannot find a single example of this (only parts of solution), so I thought it would be easy. Turns out it's not.
My issue is that I need to populate my scrollable grid layout with a list of labels from within the kivy script, using add_widget. That's because I have a variable number of of labels to add (although that number is fixed in the below example to make things simple). However the program wouldn't let me do it, saying that the ID I defined for the grid layout object is not defined. Therefore I am not able to add the labels to the grid layout.
NameError: name '_gridlayout' is not defined
Any help appreciated. Thanks
from kivy.app import App
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
Builder.load_string('''
<MainScreen>:
AnchorLayout:
anchor_x: 'center'
anchor_y: 'top'
size_hint: 1, .2
Label:
text: "Random text"
AnchorLayout:
anchor_x: 'center'
anchor_y: 'bottom'
size_hint: 1, .8
ScrollView:
GridLayout:
id: _gridlayout
cols: 1
padding: 10
spacing: 10
size_hint_y: None
width: 500
''')
class MainScreen(FloatLayout):
def __init__(self, **kwargs):
self.buildList()
super(MainScreen, self).__init__(**kwargs)
def buildList(self):
for i in range(30):
btn = Label(text=str(i), size_hint_y=None, height=40)
_gridlayout.add_widget(btn) # <- ERROR
class SMApp(App):
def build(self):
return MainScreen()
if __name__ == '__main__':
SMApp().run()
UPDATE: Corrected script below.
from kivy.app import App
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
Builder.load_string('''
<MainScreen>:
AnchorLayout:
anchor_x: 'center'
anchor_y: 'top'
size_hint: 1, .2
Label:
text: "Random text"
AnchorLayout:
anchor_x: 'center'
anchor_y: 'bottom'
size_hint: 1, .8
ScrollView:
GridLayout:
id: _gridlayout
cols: 1
padding: 10
spacing: 10
size_hint_y: None
width: 500
''')
class MainScreen(FloatLayout):
def __init__(self, **kwargs):
super(MainScreen, self).__init__(**kwargs)
self.buildList()
def buildList(self):
for i in range(30):
btn = Label(text=str(i), size_hint_y=None, height=40)
self.ids._gridlayout.add_widget(btn)
self.ids._gridlayout.bind(minimum_height=self.ids._gridlayout.setter('height'))
class SMApp(App):
def build(self):
return MainScreen()
if __name__ == '__main__':
SMApp().run()
Kivy is very picky about calling Widget.__init__() first. So if you overwrite __init__ of a widget be sure to first call super().__init__, otherwise you can get errors like you encountered or "random" crashes.
To fix change
class MainScreen(FloatLayout):
def __init__(self, **kwargs):
self.buildList()
super(MainScreen, self).__init__(**kwargs)
to
class MainScreen(FloatLayout):
def __init__(self, **kwargs):
super(MainScreen, self).__init__(**kwargs)
self.buildList()
In following code, when the button is clicked, I insert the plots into the tabs of an auinotebook in another frame.
for example, When I have multiple plots in the plt window, I can drag a notebook tab into the bottom (that results in displaying two plots). Later on when I delete the bottom tab, and try to go into other plots, I see a flicker like the closed tab is still there.
I guess the issue is with my on_nb_tab_close. Because, without that I was not able to notice any such problem.
I appreciate help. Code samples will be very useful. (wxpython version 2.812)
import wx
import wx.lib.agw.aui as aui
import matplotlib as mpl
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as mplCanvas
def create_plotter(self):
try:
self.plotter.Show()
except AttributeError:
self.plotter =PlotFrame(self, 500, 500)
self.plotter.Show()
return self.plotter
class PlotFrame(wx.Frame):
def __init__(self, parent, height, width):
wx.Frame.__init__(self, None, size=(height,width), title="plts")
self.parent=parent
self.nb = aui.AuiNotebook(self)
self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.on_nb_tab_close, self.nb)
def AddPlotTab(self,name="plot"):
page = Plot(self.nb)
self.nb.AddPage(page,name)
return page
def on_nb_tab_close(self, evt):
print "tab close fired"
s=self.nb.GetSelection()
v=self.nb.RemovePage(s)
if not self.nb.GetPageCount():
self.on_Close(evt)
evt.Veto()
class Plot(wx.Panel):
def __init__(self, parent, id = -1, dpi = None, **kwargs):
wx.Panel.__init__(self, parent, id=id, **kwargs)
self.figure = mpl.figure.Figure(dpi=dpi)
self.canvas = mplCanvas(self, -1, self.figure) # wxPanel object containing plot
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.canvas,1,wx.EXPAND)
self.SetSizer(sizer)
class MainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, title="Plotting test", size=(300, 300))
self.btn1 = wx.Button(self, -1, "Print 1")
self.Bind(wx.EVT_BUTTON, self.OnBtn1, self.btn1)
def OnBtn1(self, evt):
plotter=create_plotter(self)
page1 = plotter.AddPlotTab("case 1: first_plot")
page1.figure.gca().plot(range(10),range(10),'+')
page1.figure.gca().plot(range(10),range(10),'-',color='red')
page1.figure.canvas.draw()
if __name__ == '__main__':
APP = wx.App(False)
FRAME = MainFrame(None)
FRAME.Show()
APP.MainLoop()