Kivy references across screens - python

I want to access root widgets ids from other rootwidgets, but I can't seem to fully grasp how referencing works in Kivy and using a ScreenManager with different screens makes it even harder for me.
I want to achieve the following:
Edit: single file version
(This code assumes you're going to build a complex app, so I don't want to load all code at startup. Hence the kv_strings are loaded when switching screen, and not put into kv code of the ScreenManager. Code is based on the Kivy Showcase.)
Code main.py, Edit 2: working code (see answer why)
#!/usr/bin/kivy
# -*- coding: utf-8 -*-
from kivy.app import App
from kivy.uix.screenmanager import Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.clock import Clock
from kivy.properties import StringProperty, ObjectProperty
kv_foo = '''
<FooScreen>:
id: fooscreen_id
BoxLayout:
id: content
orientation: 'vertical'
spacing: '20dp'
padding: '8dp'
size_hint: (1, 1)
BoxLayout:
orientation: 'vertical'
Label:
id: important_text
size_hint_y: 0.3
text: app.imp_text
Button:
id: magic_change
size_hint_y: 0.3
text: "Change text above to text below (after screen switch)"
on_press: app.change_text()
ScreenManager:
id: sm
on_current_screen:
idx = app.screen_names.index(args[1].name)
'''
class FooScreen(Screen):
# 'content' refers to the id of the BoxLayout in FooScreen in foo.kv
def add_widget(self, *args):
if 'content' in self.ids:
return self.ids.content.add_widget(*args)
return super(FooScreen, self).add_widget(*args)
class FooApp(App):
imp_text = StringProperty("Should change to text from id: magic_text")
screen_magic = ObjectProperty()
magic_layout = ObjectProperty()
def build(self):
self.title = 'Foo'
self.root = root = Builder.load_string(kv_foo)
# Trying stuff with References
self.sm = self.root.ids.sm # ScreenManager
# Setting up screens for screen manager
self.screens = {}
self.available_screens = [kv_mainmenu, kv_magic]
self.screen_names = ['MainMenu', 'Magic']
self.go_screen(0)
# Go to other screen
def go_screen(self, idx):
print("Change MainScreen to: {}".format(idx))
self.index = idx
# Go to not main menu
if idx == 0:
self.root.ids.sm.switch_to(self.load_screen(idx), direction='right')
# Go to main menu
else:
self.root.ids.sm.switch_to(self.load_screen(idx), direction='left')
# Load kv files
def load_screen(self, index):
if index in self.screens:
return self.screens[index]
screen = Builder.load_string(self.available_screens[index])
self.screens[index] = screen
# if index refers to 'Magic' (kv_magic), create reference
if index == 1:
Clock.schedule_once(lambda dt: self.create_reference())
return screen
# Trying to get id's
def create_reference(self):
print("\nrefs:")
# Get screen from ScreenManager
self.screen_magic = self.sm.get_screen(self.screen_names[1])
# screen.boxlayout.magiclayout
self.magic_layout = self.screen_magic.children[0].children[0]
def change_text(self):
# Get text from id: magic_text
if self.magic_layout:
self.imp_text = self.magic_layout.ids['magic_text'].text
kv_mainmenu = '''
FooScreen:
id: mainm
name: 'MainMenu'
Button:
text: 'Magic'
on_release: app.go_screen(1)
'''
kv_magic = '''
<MagicLayout>
id: magic_layout
orientation: 'vertical'
Label:
id: magic_text
text: root.m_text
FooScreen:
id: magic_screen
name: 'Magic'
MagicLayout:
id: testmagic
'''
class MagicLayout(BoxLayout):
m_text = StringProperty("Reference between widgets test")
if __name__ == '__main__':
FooApp().run()
Question
How can I set up proper references that the button "Change text above..." can retrieve magic_text.text ("Reference between widgets test") and change self.imp_text to magic_text.text?

I found a way to reference to Kivy widgets not loaded at the startup of the app without using globals. Thanks #inclement for ScreenManager.get_screen().
I had to add the following code:
class FooApp(App):
screen_magic = ObjectProperty()
magic_layout = ObjectProperty()
...
# Trying to get id's
def create_reference(self):
print("\nrefs:")
# Get screen from ScreenManager
self.screen_magic = self.sm.get_screen(self.screen_names[1])
# screen.boxlayout.magiclayout
self.magic_layout = self.screen_magic.children[0].children[0]
def change_text(self):
# Get text from id: magic_text
if self.magic_layout:
self.imp_text = self.magic_layout.ids['magic_text'].text
self.screen_magic is assigned the screen I need (<FooScreen>) and self.magic_layout is assigned the widget I need (<MagicLayout>). Then I can use the ids from <MagicLayout> to access the Label magic_text's text.
(For full code see updated question)

Related

Kivy MD DropDownItem: how to set the item of a dropdownitem into another dropdownitem

I'm trying to developed a simple GUI in Kivy MD / Python. Originally, I modified the example code:
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.uix.list import OneLineIconListItem
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.dropdownitem import MDDropDownItem
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
MDScreen
MDDropDownItem:
id: drop_item_1
pos_hint: {'center_x': .5, 'center_y': .8}
text: 'FREQUENCY_1'
on_release: app.menu_sampling_rate_1.open()
MDDropDownItem:
id: drop_item_2
pos_hint: {'center_x': .5, 'center_y': .4}
text: 'FREQUENCY_2'
on_release: app.menu_sampling_rate_2.open()
'''
class MainApp(MDApp):
sampling_rate = ['300 Hz', '200 Hz', '100 Hz']
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
self.menu_sampling_rate_1, self.sampling_rate_items_1 = self.Create_DropDown_Widget(self.screen.ids.drop_item_1, self.sampling_rate)
self.menu_sampling_rate_2, self.sampling_rate_items_2 = self.Create_DropDown_Widget(self.screen.ids.drop_item_2, self.sampling_rate)
def Create_DropDown_Widget(self, drop_down_item, item_list):
items_collection = [
{
"viewclass": "OneLineListItem",
"text": item_list[i],
"height": dp(56),
"on_release": lambda x = item_list[i]: self.Set_DropDown_Item(drop_down_item, menu, x),
} for i in range(len(item_list))
]
menu = MDDropdownMenu(caller=drop_down_item, items=items_collection, width_mult=2)
menu.bind()
return menu, items_collection
def Set_DropDown_Item(self, dropDownItem, dropDownMenu, textItem):
dropDownItem.set_item(textItem)
dropDownMenu.dismiss()
def build(self):
return self.screen
if __name__ == '__main__':
MainApp().run()
I tried to slightly modify it using a class View in which all methods and properties related to the interface are included.
from kivy.lang import Builder
from kivy.metrics import dp
from kivymd.uix.list import OneLineIconListItem
from kivymd.app import MDApp
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.dropdownitem import MDDropDownItem
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
<View>:
orientation: vertical
MDDropDownItem:
id: drop_item_1
pos_hint: {'center_x': .5, 'center_y': .8}
text: 'FREQUENCY_1'
on_release: root.menu_sampling_rate_1.open()
MDDropDownItem:
id: drop_item_2
pos_hint: {'center_x': .5, 'center_y': .4}
text: 'FREQUENCY_2'
on_release: root.menu_sampling_rate_2.open()
'''
class View(MDBoxLayout):
sampling_rate = ['300 Hz', '200 Hz', '100 Hz']
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.menu_sampling_rate_1, self.sampling_rate_items_1 = self.Create_DropDown_Widget(self.ids.drop_item_1, self.sampling_rate)
self.menu_sampling_rate_2, self.sampling_rate_items_2 = self.Create_DropDown_Widget(self.ids.drop_item_2, self.sampling_rate)
def Create_DropDown_Widget(self, drop_down_item, item_list):
items_collection = [
{
"viewclass": "OneLineListItem",
"text": item_list[i],
"height": dp(56),
"on_release": lambda x = item_list[i]: self.Set_DropDown_Item(drop_down_item, menu, x),
} for i in range(len(item_list))
]
menu = MDDropdownMenu(caller=drop_down_item, items=items_collection, width_mult=2)
menu.bind()
return menu, items_collection
def Set_DropDown_Item(self, dropDownItem, dropDownMenu, textItem):
dropDownItem.set_item(textItem)
dropDownMenu.dismiss()
class MainApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.view = View()
def build(self):
return self.view
if __name__ == '__main__':
MainApp().run()
My questions are:
In this second version, with the View class, why I get the AttributeError: 'super' object has no attribute 'getattr'?
How to set the item of a dropdownitem equal to the current item of the second dropdownitem and viceversa? In this way when the user selects a new item into a dropdownitem, this new selection appears also into the other dropdownitem.So the two dropdownitem show the same current item.
How to set the width of a dropdownitem equal to dp(80)? The approach based on size_hint_x and width seems to not work.
Is there a way to enable/disable a dropdownitem? The property active seems to not work.
Thank you in advance for any suggestions.
In this second version, with the View class, why I get the
AttributeError: 'super' object has no attribute 'getattr'?
Because at the moment of executing init of class View(MDBoxLayout) you dont have anything in self.ids
If you try it in debug you will see it:
Solution here to create widgets after Kivy created own objects, in you first example you do it in MainApp class and thats ok.
How to set the item of a dropdownitem equal to the current item of the
second dropdownitem and viceversa? In this way when the user selects a
new item into a dropdownitem, this new selection appears also into the
other dropdownitem.So the two dropdownitem show the same current item.
If you want to change another widget(s) - just use ids to change them after you changed first one. Example of possible implementation:
def Set_DropDown_Item(self, dropDownMenu, textItem):
wanted_dropdowns = ('drop_item_1', 'drop_item_2') # ids of widgets you want to modify
for dropdown in wanted_dropdowns:
self.screen.ids[dropdown].set_item(textItem)
dropDownMenu.dismiss()
How to set the width of a dropdownitem equal to dp(80)? The approach
based on size_hint_x and width seems to not work.
Widget size changes to fit text, so maybe font_size: "80dp" is what you want.
Is there a way to enable/disable a dropdownitem? The property active
seems to not work.
Use disabled property. In python:
def disable_widget(self, widget):
widget.disabled = True
In kv
disabled: 'True'

Instance of a custom class assigned to Kivy ObjectProperty doesn't update GUI

I am trying to use Kivy Properties to refresh GUI after property is changed. However, when I try to assign an instance of custom class to property it doesn't work. After modifying property GUI is not refreshed with new value.
MWE:
python file:
from typing import List
from kivy.app import App
from kivy.properties import ObjectProperty
from kivy.uix.screenmanager import Screen
class SomeStruct:
def __init__(self, atr1: List[int], atr2: str):
self.atr1 = atr1
self.atr2 = atr2
class Root(Screen):
prop1 = ObjectProperty(None)
prop2 = ObjectProperty(None)
def __init__(self, **kw):
super().__init__(**kw)
self.prop1 = SomeStruct(atr1=[1, 2, 3], atr2="test1")
self.prop2 = "control1"
def modify1(self):
self.prop1.atr1 = [3, 2, 1]
def modify2(self):
self.prop1.atr2 = "test2"
def modify3(self):
self.prop2 = "control2"
class Mwe(App):
pass
if __name__ == '__main__':
Mwe().run()
kv file:
Root:
BoxLayout:
orientation: 'vertical'
Button:
text: str(root.prop1.atr1)
on_press: root.modify1()
Button:
text: root.prop1.atr2
on_press: root.modify2()
Button:
text: root.prop2
on_press: root.modify3()
Button 1 and 2 change classe's varaibles but text on these buttons is not updated. Button 3 works as it should.
Any help would be aprreciated, I really want to avoid making every varaible from class a separate property.

How to drag a widget in Python kivy?

I'm making an App in kivy, and I want to be able to drag a widget anywhere in my "tacscreen" how can I do that? I have a GridLayout with an Image in my "tacscreen". Below is my code! I already looked at the documents still couldn't find a solution. Any help is appreciated! Thank You!
main.py
from kivy.app import App
from kivy.uix.screenmanager import Screen
from kivy.clock import Clock
from functools import partial
class StartScreen(Screen):
pass
class TacScreen(Screen):
pass
class MainApp(App):
def on_start(self):
Clock.schedule_once(partial(self.change_screen, "tac_screen"), 5)
def change_screen(self, screen_name, *args):
self.root.current = screen_name
MainApp().run()
tacscreen.kv
#:import utils kivy.utils
<TacScreen>:
canvas.before:
Rectangle:
pos: self.pos
size: self.size
source: "Game.png"
GridLayout:
rows: 1
pos: 0, 102
size_hint: 1, .1
Image:
source: "Pencil.png"
In the official Kivy documentation there is an example that you may find helpful.
I'm not familiar with the .kv file so I use kivy.lang.Builder.load_string instead
and one small thing is that seems like the build function which helps build the GUI part is missing ( maybe ) just added
my solution is :
when pointer is touch inside the widget ( by binding on_touch_move as follow ) , the position of the center of the widget ( you can also change it by <widget_instance>.<x / right / y / top / etc> = value ) will be the position of the pointer touch
when releases , the widget will also adjust itself too : ) from the funciton on_touch_up
so the full code will be as follow :
import kivy
from kivy.app import App
from kivy.uix.screenmanager import Screen, ScreenManager # added ScreenManager
from kivy.clock import Clock
# from functools import partial
kivy.lang.Builder.load_string("""
#:kivy 2.0.0
#:import utils kivy.utils
<StartScreen>: # added
Label:
text: "Hello World !"
font_size: 25
size: self.texture_size
<TacScreen>:
# I skipped the canvas , add it back later lol : )
GridLayout:
id: mygridlayout # add an id to the widget ( a name which represent the widget )
rows: 1
pos: 0, 102
size_hint: 1, .1
Button: # widget
text: "HI !"
# skipped the Image part
""")
class StartScreen(Screen):
pass
class TacScreen(Screen):
def on_touch_up(self, touch):
# to prevent it dragging from out the screen
if (self.ids.mygridlayout.x < 0) or (
self.ids.mygridlayout.right > self.right):
self.ids.mygridlayout.x = 0
if self.ids.mygridlayout.top > self.top:
self.ids.mygridlayout.top = self.top
if self.ids.mygridlayout.y < 0:
self.ids.mygridlayout.y = 0
def on_touch_move(self, touch): # bind when pointer touch + move on screen
# use self.ids.<widget_id> to get the instance of widget
if (self.ids.mygridlayout.x < touch.x < self.ids.mygridlayout.right) and (
self.ids.mygridlayout.top > touch.y > self.ids.mygridlayout.y):
self.ids.mygridlayout.center = (touch.x, touch.y)
class MainApp(App):
screen_manager = ScreenManager()
def on_start(self):
Clock.schedule_once(lambda dt: self.change_screen("tac_screen"), 5)
def change_screen(self, screen_name: str, *args):
# self.root.current = screen_name
self.screen_manager.current = screen_name
def build(self): # we use this method build to return stuff to the GUI window ( maybe )
# add screens to screen manager
self.screen_manager.add_widget(StartScreen(name = "start_screen"))
self.screen_manager.add_widget(TacScreen(name = "tac_screen"))
# screen manager transition direction
self.screen_manager.transition.direction = "up" # left, right, up, down
self.on_start() # maybe you want to call it here ?
return self.screen_manager
MainApp().run()
any suggestions / improvement etc. is welcome : )

python kivy app stops functioning after adding widget (on PC)

I removed everything superfluous, leaving only what was necessary to reproduce the same behavior.
There is an MD Text Field in which, when entering text, if there are matches, MDDropdownMenu appears with options to choose from. The options are stored in the P_LIST list. If you don't enter text into this Mytextfield, everything works. As soon as you enter the text, the function is triggered, a menu appears, you select. After that, the application does not function.
I determined that this is happening because of the line: self.add_widget(list drop down) # <----------- marked in the code
The menu appears without add_widget, but if you enter more than one letter, a new instance of the ListDropdownValue class is created each time and the menus overlap.
#kivymd 0.104.2
#kivy 2.0.0
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix.menu import MDDropdownMenu
kv_str = """
<StartScreen>:
startscreen_textfield_1: textfield_id
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint: 1, 0.5
BoxLayout:
size_hint: 1, 0.5
orientation: "vertical"
BoxLayout:
MDTextField:
id: textfield_id
on_text:
root.open_listdropdown(textfield_id)#
BoxLayout:
MDTextField:
BoxLayout:
MDTextField:
"""
P_LIST = ["ASD", "SDF", "AASD"]
def search_product(prefix):
filtered_list = []
filtered_list = list(filter(lambda l: l.startswith(prefix), P_LIST))
return filtered_list
class MyListDropdownValue(MDDropdownMenu):
def __init__(self, dropdown_list, **kwargs):
super().__init__(**kwargs)
self.dropdown_list_id = dropdown_list
def list_dropdown(self):
if len(self.dropdown_list_id.text) != 0:
prefix = self.dropdown_list_id.text.upper()
filtered_list = search_product(prefix)
menu_items = [{'text':f'{value}',
"height": dp(56),
"viewclass": "OneLineListItem",
"on_release": lambda x= f"{value}": self.set_item(x)}
for value in filtered_list]
self.menu = MDDropdownMenu(
caller=self.dropdown_list_id,
items=menu_items,
width_mult=5,
)
self.menu.open()
def set_item(self, value):
def set_item(interval):
self.dropdown_list_id.text = value
self.menu.dismiss()
Clock.schedule_once(set_item, 0.1)
class StartScreen(BoxLayout):
startscreen_textfield_1 = ObjectProperty()
def open_listdropdown(self, id):
if len(self.children) == 1:
listdropdown = MyListDropdownProduct(id)
self.add_widget(listdropdown)
self.children[0].list_dropdown()
else:
self.children[0].menu.dismiss()
self.children[0].list_dropdown()
kv = Builder.load_string(kv_str)
class Program(MDApp):
def build(self):
self.screen = StartScreen()
return self.screen
def main():
app = Program()
app.run()
if __name__ == "__main__":
main()
Your MyListDropdownValue(MDDropdownMenu) class inherits from a MDDropdownMenu.
Then you make dropdown instance with
openlistdrop_down = MyDropdownValue(id)
Then you add that instance every time you "on_text" with
self.add_widget(listdropdown)
So you are adding multiple dropdowns.
In Kv try changing
on_text:
root.open_listdropdown(textfield_id)
To
on_validate:
root.open_listdropdown(textfield_id)
Then the user will need to hit enter before the list is made instead of with every letter added.

How do I display a variable image with a KV file?

I am trying to make a card game with Python and Kivy and can not get the card to display. So far, ChaseTheAce.deal pick a random card and removes it from the deck. I can not figure out how to pass the string from the card dict to the Image in the KV file. I'm new to Kivy and am having trouble with the IDs and what information matches with information from the PY file. Any help is appreciated, thanks!
PY File
import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.image import Image
from kivy.uix.widget import Widget
from kivy.properties import StringProperty
import random
import numpy as np
cards = ['ace_spades', 'king_spades']
dict = {'ace_spades':'ace_of_spades.png', 'king_spades':'king_of_spades2.png'}
class LoginScreen(Screen):
pass
class GameScreen(Screen):
pass
class ScoreScreen(Screen):
pass
class WindowManager(ScreenManager):
pass
class ChaseTheAce(App):
cardimagefile = StringProperty()
def deal(self):
mycard = random.choice(cards)
cards.remove(mycard)
cardimagefile = (dict[mycard])
def build(self):
return kv
kv = Builder.load_file("cta1.kv")
if __name__ == "__main__":
ChaseTheAce().run()
KV File
<GameScreen>:
ChaseTheAce:ChaseTheAce
name: "GameScreen"
GridLayout:
rows: 3
Image:
id: cardimage
source: ChaseTheAce.cardimagefile #<<<<<<<<<<<<<<<<<<<
allow_stretch: True
GridLayout:
dealer:deal
cols: 3
Button:
id: deal
text: "Deal"
on_release:
app.deal()
Edited to remove unrelated parts of the code.
There are a few errors in your example:
You need a root layout that contains all the stuff of your app.
You need a ScreenManager to manage your Screens (that you must put inside it).
To access your cardimagefile property in your .py file, you have to use self.cardimagefile, otherwise you are just creating a new, different, local cardimagefile variable.
To access your cardimagefile property in your .kv file, you have to use app.cardimagefile
Since I don't have your images, I added a print whenever the source of the Image is changing, and it works fine..
The py code:
cards = ['ace_spades', 'king_spades']
dict = {'ace_spades':'ace_of_spades.png', 'king_spades':'king_of_spades2.png'}
Builder.load_file("cta1.kv")
class LoginScreen(Screen):
pass
class GameScreen(Screen):
pass
class ScoreScreen(Screen):
pass
class WindowManager(ScreenManager):
pass
class RootLayout(FloatLayout): # create a root layout
pass
class ChaseTheAce(App):
cardimagefile = StringProperty()
def deal(self):
mycard = random.choice(cards)
cards.remove(mycard)
self.cardimagefile = (dict[mycard]) # the cardimagefile is not local needs self.
def build(self):
return RootLayout() # you must return the root layout here
if __name__ == "__main__":
ChaseTheAce().run()
The kv code:
<RootLayout>: # you need a root layout
WindowManager: # that contains a ScreenManager
GameScreen: # that manages the screens
# ChaseTheAce:ChaseTheAce # you don't need this
name: "GameScreen"
GridLayout:
rows: 3
Image:
id: cardimage
source: app.cardimagefile #<<<<<<<<<<<<<<<<<<<
allow_stretch: True
on_source: print(self.source)
GridLayout:
# dealer:deal # you don't need this
cols: 3
Button:
id: deal
text: "Deal"
on_release:
app.deal()

Categories