将 Matplotlib Graph 添加到 KivyMD 中的小部件

问题描述

我目前正在使用 KivyMD 创建一个移动应用程序,用于管理差旅费用请求。用户将在 MDTextField 上为不同类型的费用输入所需的请求金额。我想将用 patplotlib 制作的 donut添加到 MDBoxLayout 中。当请求被填满时,这样的图表应该自动更新。 (为清楚起见,我将附上屏幕截图。红色方块是我的图表所需的位置)。

enter image description here

我创建了一个名为 update_method_graph 的方法并使用了固定数字,我可以成功创建一个 Plot,但是我没有成功在应用程序上添加这样的图形。一旦我可以成功地将图形添加到我的应用程序中,我会将这些值链接用户添加的请求。现在我关心的是正确添加图表。当然,完成的代码不会包含 plt.show() 行,图表应该直接在应用程序上更新。

enter image description here

至于现在,当我关闭图形窗口时,我的代码显示错误

self.ids.expense_graph.add_widget(figureCanvasKivyAgg(plt.gcf()))
File "kivy\properties.pyx",line 863,in kivy.properties.ObservableDict.__getattr__
 AttributeError: 'super' object has no attribute '__getattr__'`

expense_graph 中的关键错误

我已尝试使用 an answer to a similar question 中建议的 from kivy.garden.matplotlib.backend_kivyagg import figureCanvasKivyAggmatplotlib.use('module://kivy.garden.matplotlib.backend_kivy'),就像 examples of use in garden.matplotlib 中所做的那样,但是我仍然无法让我的应用程序运行。

最小可复制示例的代码

Python 代码

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
import matplotlib.pyplot as plt
from kivy.garden.matplotlib.backend_kivyagg import figureCanvasKivyAgg
from kivy.uix.image import Image


class MyContentAliment(BoxLayout):
    monto_alimento = 0

    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()

    # USE THIS METHOD TO UPDATE THE VALUE OF ALIMENTOS (donut)
    def update_requested_value(self):
        MyContentAliment.monto_alimento = 0
        if len(self.ids.monto_aliment_viaje.text) > 0:
            MyContentAliment.monto_alimento = self.ids.monto_aliment_viaje.text
        else:
            MyContentAliment.monto_alimento = 0  
        TravelManagerWindow.update_donut_graph(MyContentAliment.monto_alimento)

class MyContentCasetas(BoxLayout):
    monto_casetas = 0
    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()

    # USE THIS METHOD TO UPDATE THE VALUE OF CASETAS (donut)
    def update_requested_value(self):
        MyContentCasetas.monto_casetas = 0
        if len(self.ids.monto_casetas_viaje.text) > 0:
            MyContentCasetas.monto_casetas = self.ids.monto_casetas_viaje.text
        else:
            MyContentCasetas.monto_casetas = 0
        TravelManagerWindow.update_donut_graph(MyContentCasetas.monto_casetas)


class MyContentGasolina(BoxLayout):
    monto_gasolina = 0

    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()

    # USE THIS METHOD TO UPDATE THE VALUE OF GASOLINA (donut)
    def update_requested_value(self):
        MyContentGasolina.monto_gasolina = 0
        if len(self.ids.monto_gas_viaje.text) > 0:
            MyContentGasolina.monto_gasolina = self.ids.monto_gas_viaje.text
        else:
            MyContentGasolina.monto_gasolina = 0             
        TravelManagerWindow.update_donut_graph \
            (MyContentGasolina.monto_gasolina)

class LoginWindow(Screen):
    pass


class TravelManagerWindow(Screen):
    panel_container = ObjectProperty(None)
    expense_graph = 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")))

    def update_donut_graph(self):
        travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
        travel_manager.ids.expense_graph.clear_widgets()
        # create data
        names = 'Alimentación','Casetas','Gasolina',data_values = [MyContentAliment.monto_alimento,MyContentCasetas.monto_casetas,MyContentGasolina.monto_gasolina]

        # Create a white circle for the center of the plot
        my_circle = plt.Circle((0,0),0.65,color='white')
        # Create graph,add and place percentage labels
        # Add spaces to separate elements from the donut
        explode = (0.05,0.05,0.05)
        plt.pie(data_values,autopct="%.1f%%",startangle=0,pctdistance=0.80,labeldistance=1.2,explode=explode)

        p = plt.gcf()
        p.gca().add_artist(my_circle)
        # Create and place legend of the graph
        plt.legend(labels=names,loc="center")
        # Add graph to Kivy App
        plt.show()
        # THE DESIRED RESULT IS TO ADD THE GRAPH TO THE APP WITH THE LINE OF CODE BELOW,INSTEAD OF THE plt.show() line
        travel_manager.ids.expense_graph.add_widget(Image(source='donut_graph_image.png')) 


# WINDOW MANAGER ################################
class WindowManager(ScreenManager):
    pass


class ReprodExample3(MDApp):
    travel_manager_window = TravelManagerWindow()

    def build(self):
        self.theme_cls.primary_palette = "teal"
        return WindowManager()


if __name__ == "__main__":
    ReprodExample3().run()

KV 代码

<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,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"
                            # DESIRED LOCATION FOR THE MATPLOTLIB GRAPH
                            MDBoxLayout:
                                id: expense_graph    


<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_press:
                root.update_requested_value()
            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,"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
            #input_filter: 'float'
            required: True
            on_text: root.limit_currency()

        MDRaisedButton:
            id: boton_casetas_viaje
            pos_hint: {"x":0,"top":0.5}
            text:'Ingresar Gasto'
            on_press:
                root.update_requested_value()
            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,"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_press:
                root.update_requested_value()
            on_release:
                root.sumar_gasto()

        BoxLayout:
            size_hint_x: 0.05

对我的代码的任何建议或更正将不胜感激。非常感谢。

编辑 我设法将 MDTextFields 链接到图中的数据值。因此,图表将随着输入值而更新。每次添加一个值时,都会出现一个更新的图表,以便您可以自己查看(最小可重现示例的代码已经更新)。尽管如此,我仍然无法将图表添加到我的应用程序中。我将不胜感激您的帮助。非常感谢提前!

编辑 #2

我改变了我的方法,我决定将图形转换为图像,并将图像添加到 MDBoxLayout。 (如果第一种方法更好,请告诉我)。代码已经更新。但是我收到一个错误

self.ids.expense_graph.add_widget(updated_graph)
 AttributeError: 'str' object has no attribute 'ids'

我在网络上搜索了此错误的不同解决方案,但我无法解决此问题。

编辑 3

所以我终于能够解决编辑 2 中描述的错误代码。我能够将我的图形正确添加到应用程序中。然而,该图表不会随着新费用的更新而更新(尽管文件会更新并且 plt.show() 代码行确实显示了更新的图表)。知道为什么应用程序中的图表无法更新吗?最小可重现示例的代码已更新。

enter image description here

解决方法

我认为您只需要在每次更改时重建情节。尝试将您的 update_donut_graph() 更改为:

def update_donut_graph(self):
    plt.clf()  # clear the plot
    travel_manager = MDApp.get_running_app().root.get_screen('travelManager')
    travel_manager.ids.expense_graph.clear_widgets()
    # create data
    names = 'Alimentación','Casetas','Gasolina',data_values = [MyContentAliment.monto_alimento,MyContentCasetas.monto_casetas,MyContentGasolina.monto_gasolina]

    # Create a white circle for the center of the plot
    my_circle = plt.Circle((0,0),0.65,color='white')
    # Create graph,add and place percentage labels
    # Add spaces to separate elements from the donut
    explode = (0.05,0.05,0.05)
    plt.pie(data_values,autopct="%.1f%%",startangle=0,pctdistance=0.80,labeldistance=1.2,explode=explode)

    p = plt.gcf()
    p.gca().add_artist(my_circle)
    # Create and place legend of the graph
    plt.legend(labels=names,loc="center")
    # Add graph to Kivy App
    # plt.show()
    # THE DESIRED RESULT IS TO ADD THE GRAPH TO THE APP WITH THE LINE OF CODE BELOW,INSTEAD OF THE plt.show() line
    travel_manager.ids.expense_graph.add_widget(FigureCanvasKivyAgg(figure=plt.gcf()))
,

感谢您的回复@John Anderson。和往常一样,你帮了大忙。 然而,我遇到了一个简单的问题。我的图形的大小被修改,它小得多,但图例的大小保持不变。因此,现在图例并没有按照我想要的方式显示图表,而是覆盖了图表。

enter image description here

有什么办法可以防止这种情况发生吗?我一直在想,也许一种使图形占据整个画布大小的方法是合适的。正如你在 donut_graph_image.png 截图中看到的那样,图形周围有很多空白。或者可以帮助我保持图形大小的命令?我尝试使用 Axes.set_aspect 但这确实有效。

再次感谢。

enter image description here