问题
I am currently building an app that serves as a travel expenses manager. I have a system in which the user writes the desired amount of the expense on an MDTextField, such expense will be recorded in an MDList and the total amount of requested expenses will be added to an MDTextField. For clarity I will post a screen shot:
All this system works correctly so far. However, there can be mistakes made by the user. I want the user to be able to delete an item from the MDList when the trash-can icon is pressed. At the same time, the amount of the deleted expenditure should be substracted from the total amount requested. (i.e. If the user deletes the element of Alimentación which had $1,000.00, the total amount requested after deletion should be $2000.0).
In my code I've been able to bind the on_pressed event of the icon to a remove_item function. Which succesfully does the desired substraction, and afterwards will show a success toast. However this is done without actually deleting the item from the MDList. As shown on the second screenshot, the total expenses amount is 0, nevertheless the MDList item has not been deleted (remove_widget function inside my remove_item function is not doing the desired action).
CODE FOR MINIMAL REPRODUCIBLE EXAMPLE:
Python code:
from kivy.properties import ObjectProperty
from kivy.uix.screenmanager import ScreenManager, Screen
from kivymd.app import MDApp
from kivymd.uix.expansionpanel import MDExpansionPanel, MDExpansionPanelOneLine
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix.list import TwoLineIconListItem, IconLeftWidget
from kivymd.toast import toast
class ViaticosIconList(TwoLineIconListItem):
pass
class MyContentAliment(BoxLayout):
def apply_currency_format(self):
# if len <= 3
if len(self.ids.monto_aliment_viaje.text) <= 3 and self.ids.monto_aliment_viaje.text.isnumeric():
self.ids.monto_aliment_viaje.text = "$" + self.ids.monto_aliment_viaje.text + '.00'
# n,nnn
elif len(self.ids.monto_aliment_viaje.text) == 4 and self.ids.monto_aliment_viaje.text.isnumeric():
self.ids.monto_aliment_viaje.text = "$" + self.ids.monto_aliment_viaje.text[0] + "," + \
self.ids.monto_aliment_viaje.text[1:] + '.00'
# nn,nnn
elif len(self.ids.monto_aliment_viaje.text) == 5 and self.ids.monto_aliment_viaje.text.isnumeric():
self.ids.monto_aliment_viaje.text = "$" + self.ids.monto_aliment_viaje.text[:2] + "," + \
self.ids.monto_aliment_viaje.text[2:] + '.00'
def limit_currency(self):
if len(self.ids.monto_aliment_viaje.text) > 5 and self.ids.monto_aliment_viaje.text.startswith('$') == False:
self.ids.monto_aliment_viaje.text = self.ids.monto_aliment_viaje.text[:-1]
def sumar_gasto(self):
if self.ids.monto_aliment_viaje.text == "":
pass
elif self.ids.monto_aliment_viaje.text.startswith('$'):
pass
else:
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
monto_total += float(self.ids.monto_aliment_viaje.text)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.apply_currency_format()
screen_list_view = MDApp.get_running_app().root.get_screen('travelManager').ids.viaticos_list
icon = IconLeftWidget(icon='delete')
add_item = ViaticosIconList(text=f"Alimentación Personal", secondary_text=self.ids.monto_aliment_viaje.text)
add_item.add_widget(icon)
icon.bind(on_press=self.remove_item)
screen_list_view.add_widget(add_item)
def remove_item(self, instance):
# Remove item once the trash icon is clicked (pendiente)
travel = MDApp.get_running_app().root.get_screen('travelManager')
travel.ids.viaticos_list.remove_widget(instance)
self.substract_expense()
self.show_toast()
def substract_expense(self):
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
substract_amount = self.ids.monto_aliment_viaje.text[1:-3].replace(',', '')
monto_total -= float(substract_amount)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.ids.monto_aliment_viaje.text = ''
def show_toast(self):
toast("Monto de Alimentación Personal eliminado de la solicitud")
class MyContentCasetas(BoxLayout):
def apply_currency_format(self):
# if len <= 3
if len(self.ids.monto_casetas_viaje.text) <= 3 and self.ids.monto_casetas_viaje.text.isnumeric():
self.ids.monto_casetas_viaje.text = "$" + self.ids.monto_casetas_viaje.text + '.00'
# n,nnn
elif len(self.ids.monto_casetas_viaje.text) == 4 and self.ids.monto_casetas_viaje.text.isnumeric():
self.ids.monto_casetas_viaje.text = "$" + self.ids.monto_casetas_viaje.text[0] + "," + \
self.ids.monto_casetas_viaje.text[1:] + '.00'
# nn,nnn
elif len(self.ids.monto_casetas_viaje.text) == 5 and self.ids.monto_casetas_viaje.text.isnumeric():
self.ids.monto_casetas_viaje.text = "$" + self.ids.monto_casetas_viaje.text[:2] + "," + \
self.ids.monto_casetas_viaje.text[2:] + '.00'
def limit_currency(self):
if len(self.ids.monto_casetas_viaje.text) > 5 and self.ids.monto_casetas_viaje.text.startswith('$') == False:
self.ids.monto_casetas_viaje.text = self.ids.monto_casetas_viaje.text[:-1]
def sumar_gasto(self):
if self.ids.monto_casetas_viaje.text == "":
pass
elif self.ids.monto_casetas_viaje.text.startswith('$'):
pass
else:
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
monto_total += float(self.ids.monto_casetas_viaje.text)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.apply_currency_format()
screen_list_view = MDApp.get_running_app().root.get_screen('travelManager').ids.viaticos_list
icon = IconLeftWidget(icon='delete')
add_item = ViaticosIconList(text=f"Casetas", secondary_text=self.ids.monto_casetas_viaje.text)
add_item.add_widget(icon)
icon.bind(on_press=self.remove_item)
screen_list_view.add_widget(add_item)
def remove_item(self, instance):
# Remove item once the trash icon is clicked (pendiente)
travel = MDApp.get_running_app().root.get_screen('travelManager')
travel.ids.viaticos_list.remove_widget(instance)
self.substract_expense()
self.show_toast()
def substract_expense(self):
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
substract_amount = self.ids.monto_casetas_viaje.text[1:-3].replace(',', '')
monto_total -= float(substract_amount)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.ids.monto_casetas_viaje.text = ''
def show_toast(self):
toast("Monto de Casetas eliminado de la solicitud")
class MyContentGasolina(BoxLayout):
def apply_currency_format(self):
# if len <= 3
if len(self.ids.monto_gas_viaje.text) <= 3 and self.ids.monto_gas_viaje.text.isnumeric():
self.ids.monto_gas_viaje.text = "$" + self.ids.monto_gas_viaje.text + '.00'
# n,nnn
elif len(self.ids.monto_gas_viaje.text) == 4 and self.ids.monto_gas_viaje.text.isnumeric():
self.ids.monto_gas_viaje.text = "$" + self.ids.monto_gas_viaje.text[0] + "," + \
self.ids.monto_gas_viaje.text[1:] + '.00'
# nn,nnn
elif len(self.ids.monto_gas_viaje.text) == 5 and self.ids.monto_gas_viaje.text.isnumeric():
self.ids.monto_gas_viaje.text = "$" + self.ids.monto_gas_viaje.text[:2] + "," + \
self.ids.monto_gas_viaje.text[2:] + '.00'
def limit_currency(self):
if len(self.ids.monto_gas_viaje.text) > 5 and self.ids.monto_gas_viaje.text.startswith('$') == False:
self.ids.monto_gas_viaje.text = self.ids.monto_gas_viaje.text[:-1]
def sumar_gasto(self):
if self.ids.monto_gas_viaje.text == "":
pass
elif self.ids.monto_gas_viaje.text.startswith('$'):
pass
else:
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
monto_total += float(self.ids.monto_gas_viaje.text)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.apply_currency_format()
screen_list_view = MDApp.get_running_app().root.get_screen('travelManager').ids.viaticos_list
icon = IconLeftWidget(icon='delete')
add_item = ViaticosIconList(text=f"Gasolina", secondary_text=self.ids.monto_gas_viaje.text)
add_item.add_widget(icon)
icon.bind(on_press=self.remove_item)
screen_list_view.add_widget(add_item)
def remove_item(self, instance):
# Remove item once the trash icon is clicked (pendiente)
travel = MDApp.get_running_app().root.get_screen('travelManager')
travel.ids.viaticos_list.remove_widget(instance)
self.substract_expense()
self.show_toast()
def substract_expense(self):
travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
monto_total = float(travel_manager.ids.suma_solic_viaje.text[2:])
substract_amount = self.ids.monto_gas_viaje.text[1:-3].replace(',', '')
monto_total -= float(substract_amount)
travel_manager.ids.suma_solic_viaje.text = "$ " + str(monto_total)
self.ids.monto_gas_viaje.text = ''
def show_toast(self):
toast("Monto de Gasolina eliminado de la solicitud")
class LoginWindow(Screen):
pass
class TravelManagerWindow(Screen):
panel_container = ObjectProperty(None)
# EXPANSION PANEL PARA SOLICITAR GV
def set_expansion_panel(self):
self.ids.panel_container.clear_widgets()
# FOOD PANEL
self.ids.panel_container.add_widget(MDExpansionPanel(icon="food", content=MyContentAliment(),
panel_cls=MDExpansionPanelOneLine(text="Alimentacion")))
# CASETAS PANEL
self.ids.panel_container.add_widget(MDExpansionPanel(icon="food", content=MyContentCasetas(),
panel_cls=MDExpansionPanelOneLine(text="Casetas")))
# GAS PANEL
self.ids.panel_container.add_widget(MDExpansionPanel(icon="food", content=MyContentGasolina(),
panel_cls=MDExpansionPanelOneLine(text="Gasolina")))
### WINDOW MANAGER ################################
class WindowManager(ScreenManager):
pass
class ReprodExample(MDApp):
def build(self):
self.theme_cls.primary_palette = "Teal"
return WindowManager()
if __name__ == "__main__":
ReprodExample().run()
KV File:
<WindowManager>:
LoginWindow:
TravelManagerWindow:
<LoginWindow>:
name: 'login'
MDRaisedButton:
text: 'Enter'
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
size_hint: None, None
on_release:
root.manager.transition.direction = 'up'
root.manager.current = 'travelManager'
<TravelManagerWindow>:
name:'travelManager'
on_pre_enter: root.set_expansion_panel()
MDRaisedButton:
text: 'Back'
pos_hint: {'center_x': 0.5, 'center_y': 0.85}
size_hint: None, None
on_release:
root.manager.transition.direction = 'down'
root.manager.current = 'login'
BoxLayout:
orientation: 'vertical'
size_hint:1,0.85
pos_hint: {"center_x": 0.5, "center_y":0.37}
adaptive_height:True
height: self.minimum_height
ScrollView:
adaptive_height:True
GridLayout:
size_hint_y: None
cols: 1
row_default_height: root.height*0.10
height: self.minimum_height
BoxLayout:
adaptive_height: True
orientation: 'horizontal'
GridLayout:
id: panel_container
size_hint_x: 0.6
cols: 1
adaptive_height: True
BoxLayout:
size_hint_x: 0.05
MDCard:
id: resumen_solicitud
size_hint: None, None
size: "250dp", "350dp"
pos_hint: {"top": 0.9, "center_x": .5}
elevation: 0.1
BoxLayout:
orientation: 'vertical'
canvas.before:
Color:
rgba: 0.8, 0.8, 0.8, 1
Rectangle:
pos: self.pos
size: self.size
MDLabel:
text: 'Monto Total Solicitado'
font_style: 'Button'
halign: 'center'
font_size: (root.width**2 + root.height**2) / 15.5**4
size_hint_y: 0.2
MDSeparator:
height: "1dp"
MDTextField:
id: suma_solic_viaje
text: "$ 0.00"
bold: True
line_color_normal: app.theme_cls.primary_color
halign: "center"
size_hint_x: 0.8
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
MDSeparator:
height: "1dp"
MDBoxLayout:
padding: '5dp', '10dp', '5dp', '10dp'
MDList:
pos_hint: {'x': 0, 'top': 1}
id: viaticos_list
padding: 0
<MyContentAliment>:
adaptive_height: True
MDBoxLayout:
orientation:'horizontal'
adaptive_height:True
size_hint_x:self.width
pos_hint: {"center_x":0.5, "center_y":0.5}
spacing: dp(10)
padding_horizontal: dp(10)
MDLabel:
text: 'Monto:'
multiline: 'True'
halign: 'center'
pos_hint: {"x":0, "top":0.5}
size_hint_x: 0.15
font_style: 'Button'
font_size: 19
MDTextField:
id: monto_aliment_viaje
hint_text: 'Monto a solicitar'
pos_hint: {"x":0, "top":0.5}
halign: 'left'
size_hint_x: 0.3
helper_text: 'Ingresar el monto a solicitar'
helper_text_mode: 'on_focus'
write_tab: False
required: True
on_text: root.limit_currency()
MDRaisedButton:
id: boton_aliment_viaje
pos_hint: {"x":0, "top":0.5}
text:'Ingresar Gasto'
on_release: root.sumar_gasto()
### CASETAS
<MyContentCasetas>:
adaptive_height: True
MDBoxLayout:
orientation:'horizontal'
adaptive_height:True
size_hint_x:self.width
pos_hint: {"center_x":0.5, "center_y":0.5}
spacing: dp(10)
padding_horizontal: dp(10)
MDLabel:
text: 'Monto:'
multiline: 'True'
halign: 'center'
pos_hint: {"x":0, "top":0.5}
size_hint_x: 0.15
font_style: 'Button'
font_size: 19
MDTextField:
id: monto_casetas_viaje
hint_text: 'Monto a solicitar'
pos_hint: {"x":0, "top":0.5}
halign: 'left'
size_hint_x: 0.3
helper_text: 'Ingresar el monto a solicitar'
helper_text_mode: 'on_focus'
write_tab: False
required: True
on_text: root.limit_currency()
MDRaisedButton:
id: boton_casetas_viaje
pos_hint: {"x":0, "top":0.5}
text:'Ingresar Gasto'
on_release: root.sumar_gasto()
BoxLayout:
size_hint_x: 0.05
### GASOLINA
<MyContentGasolina>:
adaptive_height: True
MDBoxLayout:
orientation:'horizontal'
adaptive_height:True
size_hint_x:self.width
pos_hint: {"center_x":0.5, "center_y":0.5}
spacing: dp(10)
padding_horizontal: dp(10)
MDLabel:
text: 'Monto:'
multiline: 'True'
halign: 'center'
pos_hint: {"x":0, "top":0.5}
size_hint_x: 0.15
font_style: 'Button'
font_size: 19
MDTextField:
id: monto_gas_viaje
hint_text: 'Monto a solicitar'
pos_hint: {"x":0, "top":0.5}
halign: 'left'
size_hint_x: 0.3
helper_text: 'Ingresar el monto a solicitar'
helper_text_mode: 'on_focus'
write_tab: False
required: True
on_text: root.limit_currency()
MDRaisedButton:
id: boton_gas_viaje
pos_hint: {"x":0, "top":0.5}
text:'Ingresar Gasto'
on_release: root.sumar_gasto()
BoxLayout:
size_hint_x: 0.05
Lastly, I would really appreciate if you could give me suggestions for code optimization on my Python file. I am repeating basically the same code in my Classes, only referring to different widgets. Can I make a list of the widgets I want to interact with and make a loop, getting the same result? I'm fairly new to Python so I guess I need to learn better ways to code.
Thanks lot in advance.
回答1:
The problem is that your remove_item()
is trying to remove the wrong object. The instance
passed to remove_item()
is the icon, not the ViaticosIconList
. In order to get the ViaticosIconList
passed to the remove_item()
you can change your bind
call to:
icon.bind(on_press=partial(self.remove_item, add_item))
The partial
essentially creates a new function that will have add_item
as an argument. See the documentation.
Then, you must adjust your remove_item()
method definition to handle the additional argument:
def remove_item(self, instance, icon):
# Remove item once the trash icon is clicked (pendiente)
travel = MDApp.get_running_app().root.get_screen('travelManager')
travel.ids.viaticos_list.remove_widget(instance)
self.substract_expense()
self.show_toast()
The icon
argument is the argument that used to be instance
and the new argument is the instance
(the add_item
).
You can simplify your classes by creating a base class for your expense types that contains all the common code, then each different expense type can just extend that base class.
来源:https://stackoverflow.com/questions/65583656/interaction-and-deletion-among-widgets-on-python-kivymd-and-code-optimization