I'm trying to create a customer management software, so I need to create a GUI. I chose Kivy because it's Open Source and LGPL.
This software is meant to have multiple panels, so I need to have ID's to access widgets in each panel. I created Kivy rules in kv language, but when I nest a class is another one, I can't access the ID's. Below an example code:
LayoutTestApp.py :
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.tabbedpanel import TabbedPanel
from kivy.uix.boxlayout import BoxLayout
class SkipperList(GridLayout):
pass
class TestPanel(BoxLayout):
def __init__(self, **kwargs):
super(TestPanel, self).__init__(**kwargs)
print "TestPanel ids:", self.ids
class MasterPanel(TabbedPanel):
pass
class NjordApp(App):
def __init__(self, **kwargs):
super(NjordApp, self).__init__(**kwargs)
def build(self):
root = MasterPanel()
return root
if __name__ == '__main__':
application = NjordApp()
application.run()
njord.kv
#:kivy 1.9.0
<MasterPanel>
pos_hint: {'center_x': .5, 'center_y': .5}
do_default_tab: False
TabbedPanelItem:
text: 'Skippers'
BoxLayout:
padding: 10
spacing: 10
TestPanel:
<TestPanel>:
id: SkipperPanelId
BoxLayout:
padding: 10
spacing: 10
BoxLayout:
orientation: 'vertical'
Label:
text: 'List des mecs'
size_hint: 1, 0.09
Button:
id: button_up
size_hint: 1, 0.08
text:'/\\'
Button:
id: button_down
size_hint: 1, 0.08
text:'\/'
When I launch the software, the print only return {}.
Can someone tell me how to access button_up ID for example ?
Thanks advance.
The reason you don't see the ids is because you are printing in the constructor of TestPanel. It hasn't finished being created yet, let alone have anything added to it. If you print the ids after the GUI has been created (ie, from a button press) then you will see the ids:
class TestPanel(BoxLayout):
def __init__(self, **kwargs):
super(TestPanel, self).__init__(**kwargs)
print "TestPanel ids:", self.ids
def test(self, *x):
print self.ids
...
Button:
id: button_up
size_hint: 1, 0.08
text:'/\\'
on_press: root.test()
output:
{'button_down': <WeakProxy to <kivy.uix.button.Button object at 0x7f63159052c0>>, 'button_up': <WeakProxy to <kivy.uix.button.Button object at 0x7f63158f3738>>}
Related
I am building a dynamic user interface using Python and Kivy. Because I need to add and remove widgets dynamically I want to use a separate class to handle adding and removing widgets from a GridLayout. I called this class LayoutManager.
The GridLayout is defined in my kv-File (id: "config_box_layout"). Inside my root widget class in my python code I am referencing the GridLayout via id. This is working properly. This reference is passed to the LayoutManager in the constructor. I tried passing it via ObjectProperty or GridLayout.
The problem is that I always end up with this kind of error if I try to remove widgets from the Layout:
'kivy.properties.ObjectProperty' object has no attribute 'remove_widget'
If I try to remove a widget in the save-method inside my Tab class using config_box_layout.remove_widget(some newly created Label) it's working properly.
I think the problem is that Kivy and all the kv-widgets are weakly-referenced and handling those references over to other classes seem to be not the intended use case.
I try to seperate classes to avoid doing all the coding stuff in one big fat main Layout class.
Looking forward to any help! :)
main.py
import kivy
from util.layoutManager import LayoutManager
from kivy.app import App
kivy.require('1.11.1')
from kivy.uix.label import Label
from kivy.uix.tabbedpanel import TabbedPanel
from kivy.properties import ObjectProperty
from kivy.uix.popup import Popup
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from functools import partial
class ChooseDirectoryDialog(FloatLayout):
add = ObjectProperty(None)
cancel = ObjectProperty(None)
save = ObjectProperty(None)
class Tab(TabbedPanel):
config_add_src = ObjectProperty()
ingest_layout = ObjectProperty()
configManager = ConfigManager()
config_box_layout = ObjectProperty()
layoutManager = LayoutManager(config_box_layout)
def add_src(self):
content = ChooseDirectoryDialog(cancel=self.cancel, save=self.save)
self._popup = Popup(title="Add Source", content=content,
size_hint=(0.9, 0.9))
self._popup.open()
def cancel(self):
self._popup.dismiss()
def delete_source(self, description, widget):
self.configManager.delete_source(description)
self.remove_source_from_ui(description)
def remove_source_from_ui(self, description):
self.layoutManager.remove_configuration(description)
def save(self, srcpath, destpath, description):
desc_label = Label(text=description)
desc_label.size_hint_y = None
desc_label.height = 30
self.config_box_layout.add_widget(desc_label)
srcpath_label = Label(text=srcpath)
srcpath_label.size_hint_y = None
srcpath_label.height = 30
self.config_box_layout.add_widget(srcpath_label)
destpath_label = Label(text=destpath)
destpath_label.size_hint_y = None
destpath_label.height = 30
self.config_box_layout.add_widget(destpath_label)
deleteButton = Button(text="Quelle löschen")
deleteButton.size_hint_y = None
deleteButton.height = 30
deleteButton.bind(on_press=partial(self.delete_source, description))
self.config_box_layout.add_widget(deleteButton)
self.layoutManager.add_configuration(description,
desc_label, srcpath_label, destpath_label, deleteButton)
self.configManager.add_source(description, srcpath, destpath)
self._popup.dismiss()
def copyToDestination(self, srcpath, destpath):
pass
class AutoIngest(App):
def build(self):
return Tab()
if __name__ == '__main__':
#Builder.load_file('autoingest.kv')
AutoIngest().run()
autoingest.kv
#:kivy 1.11.1
<Tab>:
do_default_tab: False
config_add_button: add_button
config_box_layout: config_box
TabbedPanelItem:
text: 'Konfiguration'
TabbedPanel:
tab_width: 200
do_default_tab: False
TabbedPanelItem:
text: 'Quellen verwalten'
StackLayout:
orientation: "lr-tb"
padding: 10
Button:
size_hint: .2, .1
id: add_button
text: 'Quelle hinzufügen'
on_press: root.add_src()
GridLayout:
id: config_box
cols: 4
Label:
size_hint_y: None
height: 30
text: "Bezeichnung"
Label:
size_hint_y: None
height: 30
text: "Quell-Pfad"
Label:
size_hint_y: None
height: 30
text: "Ziel-Pfad"
Label:
size_hint_y: None
height: 30
text: "Aktionen"
<ChooseDirectoryDialog>:
text_input: text_input
BoxLayout:
size: root.size
pos: root.pos
orientation: "vertical"
Label:
size_hint_y: None
height: 30
text: "Bezeichnung"
TextInput:
id: text_input
size_hint_y: None
height: 30
multiline: False
Label:
size_hint_y: None
height: 30
text: "Quellverzeichnis auswählen"
FileChooserListView:
id: source_chooser
Label:
size_hint_y: None
height: 30
text: "Zielverzeichnis auswählen"
FileChooserListView:
id: destination_chooser
BoxLayout:
size_hint_y: None
height: 30
Button:
text: "Cancel"
on_release: root.cancel()
Button:
text: "Add"
on_release: root.save(source_chooser.path, destination_chooser.path, text_input.text)
layoutManager.py
from kivy.uix.gridlayout import GridLayout
from kivy.properties import ObjectProperty
class LayoutManager:
#I alrey tried to pass the GridLayout itself. This didn't work either.
def __init__(self, configlayout: ObjectProperty):
self.configurations = {}
self.configlayout = configlayout
def remove_configuration(self, description):
widgets = self.configurations.get(description)
for x in widgets:
self.configlayout.remove_widget(x)
def add_configuration(self, description, *widgets):
self.configurations[description] = {'widgets': widgets}
I believe the problem is that the Tab instance ids have not been setup when your lines:
config_box_layout = ObjectProperty()
layoutManager = LayoutManager(config_box_layout)
are executed, so config_box_layout is not yet set.
You can force the creation of the LayoutManager to delay until the ids are created using kivy.clock in the build() method of the App:
class AutoIngest(App):
def build(self):
Clock.schedule_once(self.setup_layoutManager)
return Tab()
def setup_layoutManager(self, dt):
self.root.layoutManager = LayoutManager(self.root.config_box_layout)
Also, the line:
layoutManager = LayoutManager(config_box_layout)
can be removed from the Tab class.
Once that is working you will discover issues in your remove_configuration() method of LayoutManager.
I'm trying to call a custom widget method from its ids.
But I received an AttributeError: 'LabelBox' object has no attribute 'change_first_text'.
As a simple as possible working example can be found here with the PanelApp.py file:
from kivy.app import App
from kivy.uix.tabbedpanel import TabbedPanel
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
Builder.load_file("panel.kv")
class LabelBox(BoxLayout):
def __init__(self, *args, **kwargs):
super(LabelBox, self).__init__(*args, **kwargs)
def change_first_text(self, text):
self.ids.first.text = text
class ButtonList(BoxLayout):
pass
class Test(TabbedPanel):
pass
class TabbedPanelApp(App):
def build(self):
self.test = Test()
self.btn_list = ButtonList()
self.vbox = BoxLayout(orientation="vertical")
self.vbox.add_widget(self.btn_list)
self.vbox.add_widget(self.test)
return self.vbox
def change_first(self, value):
print("Button clicked and new value is: '{}'".format(value))
self.test.ids.lbs.change_first_text(value)
if __name__ == '__main__':
TabbedPanelApp().run()
and the panel.kv file:
<ButtonList#ButtonList>:
orientation: "horizontal"
Button:
text: "change fisrt to me"
on_press: app.change_first(self.text)
Button:
text: "change two to me"
<LabelBox#BoxLayout>:
Label:
id: first
text: "first"
Label:
id: two
text: "two"
<Test>:
size_hint: .5, .5
pos_hint: {'center_x': .5, 'center_y': .5}
do_default_tab: False
TabbedPanelItem:
text: 'first tab'
LabelBox:
id: lbs
Calling the script cause a runtime error that I can't understand.
Have you any clue on how to manage such event callback through the application?
The problem in your case is that you are creating 2 classes called LabelBox:
1.
class LabelBox(BoxLayout):
def __init__(self, *args, **kwargs):
super(LabelBox, self).__init__(*args, **kwargs)
def change_first_text(self, text):
self.ids.first.text = text
2.
<LabelBox#BoxLayout>:
Label:
id: first
text: "first"
Label:
id: two
text: "two"
I understand that you only want to have a class so it is appropriate to do the creation with the inheritance in the .py and only the implementation of children in the .kv. The solution is to change delete #BoxLayout in the .kv
<LabelBox>:
Label:
id: first
text: "first"
Label:
id: two
text: "two"
I'm currently trying to create a customized MessageBox in Python 3.6 using Kivy 1.10.0. I want to use it first as a MessageBox for error message when user entered incorrect username or password.
I'm getting an attribute error whenever I call the open function from CalcPopUp class using the nextScreen function from CalcRoot class.
This is the codes that I have:
class CalcRoot(BoxLayout):
def __init__(self,**kwargs):
super(CalcRoot,self).__init__(**kwargs)
self.calc_popup = CalcPopUp(**kwargs)
def nextScreen(self, next_screen):
#I have some conditions inside this function which works fine
CalcPopUp.open(self, "Incorrect Login", True)`
class CalcPopUp(Popup):
popup_message = ObjectProperty()
popup_button = ObjectProperty()
def __init__(self, *args, **kwargs):
super(CalcPopUp,self).__init__(*args, **kwargs)
def open(self, app_message, with_button=True):
#if user selected the button attribute as true show button else remove
if with_button:
if self.popup_button in self.content.children:
self.content.remove_widget(self.popup_button)
# if answer is wrong, display button if not visible
else:
if self.popup_button not in self.content.children:
self.content.add_widget(self.popup_button)
#display text message
self.message.text = app_message
#display pop up
super(CalcPopUp, self).open()
This is the error that I'm getting:
AttributeError: 'CalcRoot' object has no attribute 'popup_button'
This is the content of the kivy file associated to my screenpop:
<CalcPopUp>:
size_hint: .8, .4
title: "Message"
title_size: root.height *.05
auto_dismiss: False
separator_color: COLOR("#fcfc02") #yellow
popup_button: popup_button
popup_message: popup_message
BoxLayout:
orientation: 'horizontal'
padding: root.width * .02, root.height * .02
spacing: min(root.height, root.width) * .02
Label:
id: popup_message
text: ""
halign: 'left'
font_size: root.height / 10
center_y: .5
markup: True
Button:
id: popup_button
text: 'Ok'
size_hint: 1, None
height: root.height / 20
on_release: root.dismiss()
Here's what I did:
First of all, remove lines 7 and 8 in the .kv file. I'm not sure whether there is an indentation error in your original post, but here's how the .kv file should look now:
<CalcPopUp>:
size_hint: .8, .4
title: "Message"
title_size: root.height *.05
auto_dismiss: False
BoxLayout: # this is indented to be inside CalcPopUp
orientation: 'horizontal'
... # etc., no changes other than indentation...
I've changed the .py file structure quite a bit, take a look and tell me if there's anything I need to make explain:
from kivy.properties import ObjectProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.popup import Popup
from kivy.base import runTouchApp
from kivy.base import Builder
class CalcRoot(BoxLayout):
def __init__(self, **kwargs):
super(CalcRoot, self).__init__(**kwargs)
def nextScreen(self, next_screen):
# I have some conditions inside this function which works fine
popup = CalcPopUp("Incorrect Login", True)
popup.open()
class CalcPopUp(Popup):
popup_message = ObjectProperty()
popup_button = ObjectProperty()
def __init__(self, app_message, with_button=True, **kwargs):
super().__init__(**kwargs)
# if user selected the button attribute as true show button else remove
if with_button:
if self.popup_button in self.content.children:
self.content.remove_widget(self.popup_button)
# if answer is wrong, display button if not visible
else:
if self.popup_button not in self.content.children:
self.content.add_widget(self.popup_button)
# display text message
self.ids.popup_message.text = app_message
Builder.load_file("calcpopup.kv")
root = CalcRoot()
root.nextScreen(next_screen=None)
runTouchApp(root)
I want to call a method add_category_to_tree() from TreeCategory class, when pressing SAVE button in AddCategoryPopup class. However, I have problems how to reference to the TreeCategory instance, which is created in KV file.
I was trying to search solution, but nothing works. Currently i get AttributeError: 'super' object has no attribute '__getattr__' error.
How should I do this properly ? Thank you for help
class TreeCategory(TreeView):
def __init__(self, **kwargs):
super(TreeCategory, self).__init__(**kwargs)
def add_category_to_tree(self, name):
self.add_node(TreeViewLabel(text = name.upper()))
class AddCategoryPopup(Popup):
def save(self):
self.ids.tree.add_category_to_tree(self.ids.entry.text) # ????
db.adding_to_db('kategorie', 'nazwa', self.ids.entry.text)
self.dismiss()
def close(self):
self.dismiss()
class MainScreen(BoxLayout):
tree = ObjectProperty(None)
def add_category_button(self):
popup = AddCategoryPopup(title = 'Dodawanie nowej kategorii')
return popup.open()
class GuiCookBookApp(App):
def build(self):
self.title = "Książka kucharska"
return MainScreen()
if __name__ == "__main__":
db = DatabaseManager("cookbook.sqlite")
GuiCookBookApp().run()
KV file:
<AddCategoryPopup>:
BoxLayout:
orientation: 'vertical'
TextInput:
id: entry
multiline: False
hint_text: 'Podaj nazwę kategorii...'
BoxLayout:
orientation: 'horizontal'
Button:
text: 'SAVE'
on_press: root.save()
Button:
text: 'CANCEL'
on_press: root.close()
<MainScreen>:
orientation: "vertical"
display: entry
tree: tree
BoxLayout:
id: menu
size_hint_y: .1
Button:
text: 'Dodaj kategorię'
on_press: root.add_category_button()
BoxLayout:
id: recipe_view
orientation: "horizontal"
TreeCategory:
id: tree
hide_root: True
size_hint: .25, 1
With self.ids in Python you can only access the ids in KV from that particular class. So self.ids.tree is only possible inside the MainScreen class in Python not in the AddCategoryPopup class.
You could create an ObjectProperty 'topwidget' in your AddCategoryPopup rule and pass in the Main class when you instantiate the popup. Something like:
popup = AddCategoryPopup(topwidget=self)
Then in the 'save' method of your custom popup class you can do something like:
self.topwidget.tree...
You can do it quite a few ways. For instance you can put the .kv in your main .py file.
w = Builder.load_string('''
Widget:
height: self.width / 2. if self.disabled else self.width
x: self.y + 50
''')
https://kivy.org/docs/api-kivy.lang.builder.html
You can simply name the .kv file
guicookbookapp.kv
and leave it in the root directory of your project.
https://kivy.org/docs/examples/gen__application__app_with_kv__py.html
You can also add the following
from kivy.lang import Builder
Builder.load_file('guicookbookapp.kv')
Hopefully I understood your question correctly.
Im making a simple app in Kivy that needs to use the FileChooserListView widget. For some reason whenever I try using the widget, it only covers the lower left hand side of the app. I can't get it to fill the entire screen like its supposed to.
Here is what happens to my app:
main.py:
import kivy
kivy.require('1.8.0')
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
class AppRoot(BoxLayout):
img_picker = ObjectProperty(None)
def __init__(self, *args, **kwargs):
super(BoxLayout, self).__init__(*args, **kwargs)
self.img_picker = ImagePicker()
def change_to_img_picker(self):
self.clear_widgets()
self.add_widget(self.img_picker)
def change_to_about_us(self):
print "change_to_about_us"
class ImagePicker(BoxLayout):
def __init__(self, *args, **kwargs):
super(BoxLayout, self).__init__(*args, **kwargs)
def open(self, path, selection):
print "OPEN"
def selected(self, selection):
print "SELECTION"
class ImageApp(App):
def build(self):
return AppRoot()
if __name__ == "__main__":
ImageApp().run()
image.kv:
AppRoot:
<AppRoot>:
orientation: 'vertical'
padding: root.width * .02, root.height * .02
spacing: 10
Label:
text: "Some text should go here"
Button:
text: "Select Image"
size_hint_y: None
height: root.height * .1
on_release: root.change_to_img_picker()
Button:
text: "About the App"
size_hint_y: None
height: root.height * .1
on_release: root.change_to_about_us()
<ImagePicker>:
orientation: 'vertical'
id: img_picker
Button:
text: "open"
on_release: root.open(filechooser.path, filechooser.selection)
size_hint: 1, .2
FileChooserListView:
id: filechooser
on_selection: root.selected(filechooser.selection)
size_hint: 1, .8
The change_to_img_picker() function should replace the current widgets with the FileChooserListView and take up all the space. I'm not sure why it's not working. I would appreciate it if anyone would enlighten me as to why this is happening. Thank You in advance:)
super(BoxLayout, self).__init__(*args, **kwargs)
The problem is that you call super on BoxLayout, not for your ImagePicker class (the first argument should be the current class, not the parent). This messes up the binding that is normally created in the BoxLayout's __init__ (probably this is simply not called any more, you get the BoxLayout parent's __init__ instead), so the normal layout triggers are absent and the change of size etc do not affect the children.
I'm not completely sure of some of the details (in particular, I'm surprised that the layout for your root widget seems to work fine), but this seems to be the core of the problem.