1

tardes o noches. Llevo varios días devanándome los sesos buscando la forma de solucionar este problema que tengo. He mirado en la documentación de Kivy, videotutoriales y blogs con ejemplos, pero no consigo dar con la forma de solucionar el problema.

El problema es que tengo dos widgets: - Uno con botones para realizar una serie de funciones - Otro donde visualizar información

El widget que muestra los botones lo único que hace es hacer un pequeño análisis de las IP conectadas a una red, otro para poner en marcha un servidor MQTT y otro para parar el servidor. => [Buscar dispositivos -- Conectar -- Desconectar]

En el widget en el que se muestra la información, la idea es que en un primer momento cuando ejecutas el código se encuentre vacío, pero cuando le des al botón que busca las IP disponibles, en el espacio vacío aparezcan todas las IP disponibles con un botón por cada IP. Mejor me explico con un pequeño ejemplo:

Widget de información vacío >> Presionar botón "Buscar dispositivos" >> Analiza y obtiene IPs disponibles >> Obtiene las IP 111.111.1.000 - 111.111.1.001 - 111.111.1.002 >> En el widget de información aparecen 3 botones uno con cada IP

Después, en cada botón con su IP, cuando se le apriete al botón conectar, debería cambiar el texto con otra información que me devolvería cada dispositivo conectado al servidor MQTT, y luego al apretarle a cada botón cambiaría la pantalla y mostraría más información y más cosas, pero eso es otro problema.

Entonces, ¿cómo puedo hacer para que llamando a la función que me busca las IP, desde el botón conectar en un widget, me devuelva la información a otro widget?

Os dejo el código que tengo hasta ahora tanto el .py como el .kv.

.py

import kivy     # Paquete general para crear la interfaz

from kivy.app import App    # Funciones para implementar nuestra ventana o App
from kivy.uix.boxlayout import BoxLayout  # Funciones para implementar una capa base donde se colocarán los elementos
from kivy.uix.anchorlayout import AnchorLayout  # Funciones para colocar elementos en sitios concretos del layout
from kivy.uix.gridlayout import GridLayout  # Funciones para organizar elementos en matrices
from kivy.uix.button import Button  # Funciones para manejar botones
from kivy.uix.listview import ListItemButton    # Funciones para manejar listas
from kivy.clock import Clock  # Necesario para actualizar los elementos
from kivy.event import EventDispatcher  # Necesario para crear propiedades y eventos
from kivy.properties import ListProperty    # Importación de propiedades de lista Kivy
from kivy.config import Config  # Para las configuraciones que sean necesarias

import os   # Paquete necesario para las funciones que requieren de recursos del sistema operativo
import subprocess   # Paquete necesario para crear y llamar a subprocesos del sistema

# Configuración del tamaño de pantalla
Config.set('graphics', 'width', 1024)
Config.set('graphics', 'height', 600)
# Los botones, widgets, layout y demás son todos widgets

lista_ip = []   # Variable vacía


class funciones():
    def ping_scan(self, *arg):  # Función para detectar dispositivos en la red. Con *arg se introducen argumentos
        # que luego no se tendrán en cuenta
        ip_list = []
        with open(os.devnull, "wb") as limbo:  # devnull es como un pozo sin fondo del que no se puede recuperar nada
            # y elimina el error por pantalla en tiempo de ejecución
            for n in range(0, 10 + 1):  # Se añade el + 1 para que alcance el límite máximo introducido
                ip = "192.168.1.{0}".format(n)
                res = subprocess.Popen(['ping', '-n', '1', '-w', '200', ip], stdout=limbo, stderr=limbo).wait()
                if res:
                    print("INACTIVA => " + ip)
                else:
                    print("ACTIVA => " + ip)
                    ip_list.append(ip)
        return ip_list


class Contenedor(BoxLayout):    # Creamos una clase Contenedor que hereda las funciones de BoxLayout
    def __init__(self):
        super(Contenedor, self).__init__()    # Con super se heredan todas las propiedades de Contenedor. Esto es
        # necesario para añadir nuevas propiedades

        self.BB = button_box()  # Instanciación a caja para botones
        self.IB = info_box()    # Instanciación a caja para información
        self.LB = logo_box()    # Instanciación a caja para logo

        self.add_widget(self.BB)    # Añade la caja al layout
        self.BB.add_widget(self.LB)  # Añade la caja para el logo en la caja de botones
        self.add_widget(self.IB)    # Añade la caja para info


class button_box(BoxLayout):    # LayOut para añadir los botones

    def __init__(self):
        # nonlocal lista_ip   # Hacemos que este valor sea no local
        super(button_box, self).__init__()
        self.buscar = Button(text="Buscar dispositivos")  # Crea un botón para buscar dispositivos
        self.con = Button(text="Conectar")  # Crea un botón para conectarse a los dispositivos
        self.desc = Button(text="Desconectar")  # Crea un botón para desconectarse de los dispositivos
        self.add_widget(self.buscar)    # Añade el botón de buscar al LayOut
        self.add_widget(self.con)   # Añade el botón de conectar al LayOut
        self.add_widget(self.desc)  # Añade el botón de desconectar al LayOut
        lista_ip = self.buscar.bind(on_press=funciones.ping_scan)  # on_press añade argumentos a la función. Hay que añadir *arg a
        # la función. Con la función on_press detectamos si ha sido apretado un botón y hace una acción si se da la
        # condición


class info_box(BoxLayout):  # LayOut para añadir la información de los dispositivos

    def __init__(self):
        super(info_box, self).__init__()
        #lista_ip = ['0', '1', '2', '3']
        if len(lista_ip) != 0:
            for i in range(len(lista_ip)):
                self.add_widget(Button(text=lista_ip[i]))
        else:
            print("No hay IP")


class logo_box(AnchorLayout):  # LayOut para añadir el logo del programa en una esquina
    None


class interfazApp(App):  # Creación de la aplicación como tal. Debe llevar el mismo nombre que el archivo .kv
    title = 'Centro de control'  # Nombre del programa

    def build(self):    # Función para que se ponga en marcha nuestra App
        return Contenedor()


if __name__ == "__main__":  # Obligatorio, aungue no necesario, para Android y Kivy, es un convencionalismo
    interfazApp().run()

Creo que no hace falta decir que la función ping_ip es la que se encarga de hacerle ping a todas las IP que haya disponibles en la red. El que la haga entre 0 y 10 simplemente es por simplicidad a la hora de hacer las pruebas, el rango ya lo modificaré en la versión final.

.kv

<Contenedor>:
    orientation: 'vertical'
    spacing: 10
    # spacing es el espacio que hay entre widgets
    padding: 10
    # padding es el espacio entre el borde de la ventana y el contenido => iz - a - de - ab => Lista para distintos
    canvas:
    # Las instrucciones canvas son instrucciones gráficas para personalizar los widgets
        Color:
            rgb: 0, 0, 0
            # Son valores en tanto por uno. Con rgba añadimos el alfa
        Rectangle:
            size: self.size
            pos: self.pos
            # self hace referencia al widget o layout máx póximo a la indentación
            # En este caso, mismo tamaño y misma posición que Contenedor

<button_box>:
# Por defecto, las BoxLayout vienen orientadas de forma horizontal
    spacing: 10
    padding: 10
    size_hint: 1, None
    # Deshabilitación del tamaño relativo en X e Y
    # width: 650
    height: 50
    canvas:
        Color:
            # rgb: 0.78, 0.78, 0.78
            rgb: 0.65, 0.65, 0.65
        Rectangle:
            size: self.size
            pos: self.pos

<info_box>:
    orientation: 'vertical'
    spacing: 10
    padding: 10
    canvas:
        Color:
            rgba: 1, 1, 1, 0.25
        Rectangle:
            size: self.size
            pos: self.pos
#    Label:
#        markup: True
#        text: "[ref=prueba]Hola mundo[/ref]"
#        # Para que el elemento de la lista tenga un botón invisible, hay que añadir [ref=referencia]Texto[/ref]
#        # Con on_ref_press hacemos que al pinchar en la referencia creada en la línea anterior de código, se ejecute algo
#        on_ref_press: print("Hola holita")

<logo_box>:
    spacing: 2
    padding: 5
    size_hint: None, None
    width: 40
    height: 32
    canvas:
        Rectangle:
            source: 'UNIT_n.png'
            size: self.size
            pos: self.pos

Me imagino que todo lo que quiero hacer es con Properties y seguro que es más sencillo de lo que parece, pero no sé cómo hacerlo. También me gustaría, si es posible, que me comentéis si hay algún error o hay algo que se podría hacer de forma más simple.

Creo que no se me escapa nada por indicar, pero de faltar algo, hacédmelo saber y os doy la información que pueda faltar.

Muchas gracias de antemano. Saludos.

Dratcher
  • 37
  • 6

1 Answers1

1

Realmente no necesitas nada especial, ni usar variables globales o properties, puedes crear los botones dinámicamente con un simple for. Lo que si te recomiendo es que el escaneo lo hagas en un hilo aparte, en cuanto dicha función tarde un poco en retornar la interfaz se bloqueara.

No se puede interactuar de forma directa con la interfaz desde un widget, pero en Kivy es realmente simple interactuar con el hilo principal desde uno hijo, en este caso basta con llamar a un método (por lógica, de la clase InfoBox) decorado con kivi.clock.mainthread, pasarle la ip y que este cree el botón.

Podría quedar algo así:

main.py

import os   # Paquete necesario para las funciones que requieren de recursos del sistema operativo
import threading
import subprocess   # Paquete necesario para crear y llamar a subprocesos del sistema

import kivy     # Paquete general para crear la interfaz
from kivy.app import App    # Funciones para implementar nuestra ventana o App
from kivy.uix.boxlayout import BoxLayout  # Funciones para implementar una capa base donde se colocarán los elementos
from kivy.uix.anchorlayout import AnchorLayout  # Funciones para colocar elementos en sitios concretos del layout
from kivy.uix.gridlayout import GridLayout  # Funciones para organizar elementos en matrices
from kivy.uix.button import Button  # Funciones para manejar botones
from kivy.uix.listview import ListItemButton    # Funciones para manejar listas
from kivy.clock import Clock,  mainthread  # Necesario para actualizar los elementos
from kivy.event import EventDispatcher  # Necesario para crear propiedades y eventos
from kivy.properties import ObjectProperty    # Importación de propiedades de lista Kivy
from kivy.config import Config  # Para las configuraciones que sean necesarias


# Configuración del tamaño de pantalla
Config.set('graphics', 'width', 1024)
Config.set('graphics', 'height', 600)
# Los botones, widgets, layout y demás son todos widgets

def ping_scan():  # Función para detectar dispositivos en la red.
    with open(os.devnull, "wb") as limbo:  # devnull es como un pozo sin fondo del que no se puede recuperar nada
        # y elimina el error por pantalla en tiempo de ejecución
        for n in range(0, 10 + 1):  # Se añade el + 1 para que alcance el límite máximo introducido
            ip = "192.168.1.{0}".format(n)
            res = subprocess.Popen(['ping', '-n', '1', '-w', '200', ip], stdout=limbo, stderr=limbo).wait()
            if res:
                print("INACTIVA => " + ip)
            else:
                print("ACTIVA => " + ip)
                yield ip


class Contenedor(BoxLayout):    # Creamos una clase Contenedor que hereda las funciones de BoxLayout
    scanning = threading.Event()
    def __init__(self):
        super().__init__()    # Con super se heredan todas las propiedades de Contenedor. Esto es
        # necesario para añadir nuevas propiedades

        self.button_box = ButtonBox()  # Instanciación a caja para botones
        self.info_box = InfoBox()    # Instanciación a caja para información
        self.logo_box = LogoBox()    # Instanciación a caja para logo

        self.add_widget(self.button_box)    # Añade la caja al layout
        self.button_box.add_widget(self.logo_box)  # Añade la caja para el logo en la caja de botones
        self.add_widget(self.info_box)    # Añade la caja para info

        self.button_box.btn_buscar.bind(on_press=self.start_ping_scan)

    def start_ping_scan(self, event=None):
        if self.scanning.is_set():
            self.scanning.clear()
            self.button_box.btn_buscar.text = "Buscar"
        else:
            self.scanning.set()
            threading.Thread(target=self._ping_scan).start()
            self.button_box.btn_buscar.text = "Cancelar búsqueda"

    def _ping_scan(self):
        self.info_box.limpiar_info()
        for ip in ping_scan():
            if not self.scanning.is_set():
                return
            self.info_box.agregar_dispositivo(ip)


class ButtonBox(BoxLayout):    # LayOut para añadir los botones
    def __init__(self):
        # nonlocal lista_ip   # Hacemos que este valor sea no local
        super().__init__()
        self.btn_buscar = Button(text="Buscar dispositivos")  # Crea un botón para buscar dispositivos
        self.btn_conectar = Button(text="Conectar")  # Crea un botón para conectarse a los dispositivos
        self.btn_desconectar = Button(text="Desconectar")  # Crea un botón para desconectarse de los dispositivos
        self.add_widget(self.btn_buscar)    # Añade el botón de buscar al LayOut
        self.add_widget(self.btn_conectar)   # Añade el botón de conectar al LayOut
        self.add_widget(self.btn_desconectar)  # Añade el botón de desconectar al LayOut


class InfoBox(BoxLayout):  # LayOut para añadir la información de los dispositivos
    ips = ObjectProperty(None)
    def __init__(self):
        super().__init__()
        self._btn_disp = [] # Lista con las instancias d cada botón

    @mainthread  # Método llamado  desde el hilo hijo
    def agregar_dispositivo(self, ip):
        btn = Button(text=ip)
        self._btn_disp.append(btn)
        self.ips.add_widget(btn)

    @mainthread  # Método llamado  desde el hilo hijo
    def limpiar_info(self):
        for btn in self._btn_disp:
            self.ips.remove_widget(btn)
        self._btn_disp.clear()


class LogoBox(AnchorLayout):  # LayOut para añadir el logo del programa en una esquina
    pass


class InterfazApp(App):  # Creación de la aplicación como tal. Debe llevar el mismo nombre que el archivo .kv
    title = 'Centro de control'  # Nombre del programa
    def build(self):    # Función para que se ponga en marcha nuestra App
        return Contenedor()

    def on_stop(self):
        # Si cerramos la app mintras se están escanenado ips, debemos detener el hilo
        self.root.scanning.clear()


if __name__ == "__main__":  # Obligatorio, aungue no necesario, para Android y Kivy, es un convencionalismo
    InterfazApp().run()

interfaz.kv

<Contenedor>:
    orientation: 'vertical'
    spacing: 10
    # spacing es el espacio que hay entre widgets
    padding: 10
    # padding es el espacio entre el borde de la ventana y el contenido => iz - a - de - ab => Lista para distintos
    canvas:
    # Las instrucciones canvas son instrucciones gráficas para personalizar los widgets
        Color:
            rgb: 0, 0, 0
            # Son valores en tanto por uno. Con rgba añadimos el alfa
        Rectangle:
            size: self.size
            pos: self.pos
            # self hace referencia al widget o layout máx póximo a la indentación
            # En este caso, mismo tamaño y misma posición que Contenedor

<ButtonBox>:
# Por defecto, las BoxLayout vienen orientadas de forma horizontal
    spacing: 10
    padding: 10
    size_hint: 1, None
    # Deshabilitación del tamaño relativo en X e Y
    # width: 650
    height: 50
    canvas:
        Color:
            # rgb: 0.78, 0.78, 0.78
            rgb: 0.65, 0.65, 0.65
        Rectangle:
            size: self.size
            pos: self.pos

<InfoBox>:
    ips: ips
    id: info_root
    orientation: 'vertical'
    spacing: 10, 10

    canvas:
        Color:
            rgba: 1, 1, 1, 0.25
        Rectangle:
            size: self.size
            pos: self.pos

    ScrollView:
        size: self.size
        GridLayout:
            id: ips
            cols: 1
            size_hint_y: None
            height: self.minimum_height
            row_default_height: '50dp'
            row_force_default: True


<LogoBox>:
    spacing: 2
    padding: 5
    size_hint: None, None
    width: 40
    height: 32
    canvas:
        Rectangle:
            source: 'UNIT_n.png'
            size: self.size
            pos: self.pos 

He cambiado algunos nombres para intentar cumplir con PEP-8 y no confundir nombres de clases con atributos/variables.

Aparte de esto, he añadido un ScrollView y los botones de para cada IP tienen un tamaño fijo.

Los botones se van agregando según se va encontrando una nueva ip, no todos al final, para ello tu función ping_scan pasa a retornar un generador (yield ip). De todas formas si lo prefieres de esta última forma no hay problema. Así mismo, se puede interrumpir en cualquier momento la búsqueda gracias al uso de un evento para comunicarse entre los hilos.

El resultado es el siguiente:

introducir la descripción de la imagen aquí

Edición

Respondiendo a dudas planteadas en comentario:

  • El uso de un solo guion bajo como primer carácter del nombre de una variable o método es solo una convención. En Python no existen conceptos de método/atributo privado/publico/protegido, el lenguaje asume que todos somos lo suficientemente mayores para saber que estamos haciendo. En este caso, el guion bajo indica a otros programadores que se trata de un método o función "privado", que es de uso interno de la clase, por lo que no debe modificarse o llamarse directamente (aunque si quieres puedes).

  • En el caso de super, permite hacer referencia a la clase padre de forma directa y hacer referencia a sus atributos o llamar a sus métodos. En este caso super().__init__() llama al método __init__ de la clase padre, en el caso de la clase contenedor sería equivalente a BoxLayout.__init__(self). Para más información ver:

  • El métododo __init__ es el inicializador de la clase, ver:

  • @ en este caso se usa para decorar una función. Un decorador es una función de orden superior que recibe otra función como argumento y extiende su funcionalidad, retornando también una función. Por ejemplo, imaginemos que queremos registrar cunado se ejecutan ciertas funciones, podemos hacer lo siguiente:

    def logging(func):
        def inner_func(*args, **kwargs):
            from  datetime import datetime
            print(f"{datetime.now()} -> Ejecutada {func.__qualname__}")
            result = func(*args, **kwargs)
            return result
        return inner_func
    

    logging es un decorador, recibe una función cualquiera y le añade la funcionalidad de registrar la fecha en la que se ejecuta, para decorar cualquier función usamos @logging:

    @logging
    def foo():
        print("Hola desde foo")
    
    @logging    
    def suma(a, b):
        print(f"{a} + {b} = {a + b}")
    

    si ejecutamos las funciones:

    >>> foo()
    
    2019-05-02 12:26:48.324331 -> Ejecutada foo
    Hola desde foo
    
    >>> suma(3, 7)
    2019-05-02 12:26:48.324664 -> Ejecutada suma
    3 + 7 = 10
    

    En nuestro caso concreto mainthread extiende la funcionalidad de un método/función para que sea llamado desde un hilo hijo pero que se ejecute en el hilo principal. Esto es necesario porque no se puede interactuar directamente jamás con la interfaz y sus widgets desde un hilo hijo, como ocurre con muchos otros frameworks gráficos, derivado generalmente de cómo funciona OpenGl.

FJSevilla
  • 55,603
  • 7
  • 35
  • 58
  • ¡¡¡MIL GRACIAS!!! Acabo de probarlo y ejecutar todo en el ordenador y me funciona perfecto. Ahora a seguir programando el resto de funciones y terminar de entender cómo funciona el código que me has pasado y sobretodo, Kivy. Te estaría preguntando cosas de Kivy sin parar, pero eso ya es excederse. De verdad, ¡muchísimas gracias! – Dratcher May 01 '19 at 10:43
  • ¡De nada @Dratcher! Y no te cortes en preguntar, así que si tienes alguna duda sobre esta respuesta comenta, que yo por mi parte intentaré responder lo mejor que pueda y sepa. Si tienes otras preguntas diferentes tampoco dudes en formular una nueva, cuanta más documentación en español generemos entre todos (que no abunda...) , mejor. Un saludo. – FJSevilla May 01 '19 at 12:56
  • Sí, tienes toda la razón, hay muy poca documentación y, de hecho, sí que hay unas cuantas cosas que me gustaría preguntarte. Una es que has llamado a la lista en la que se instancia cada botón como _btn_disp. ¿Que empiece por guion bajo es por algo en concreto? No sé si será algún convencionalismo, pero me ha llamado mucho la atención. También me gustaría saber cómo funciona super().__init__() Tanto super como __init__ son dos elementos bastante nuevos para mí y no tengo muy claro aún cómo funcionan. De hecho, ¿para qué son las @? Eso sí que es la primera vez que me lo encuentro.Muchas gracias – Dratcher May 02 '19 at 00:24
  • @Dratcher he editado la respuesta, al final tienes una explicación y algunos enlaces a otras preguntas del sitio directamente relacionadas con tus dudas. Espero que te ayude a entender mejor el código de la respuesta. – FJSevilla May 02 '19 at 10:52
  • Así sí. Así da gusto ponerse a aprender cosas nuevas. ¡Muchísimas gracias! – Dratcher May 03 '19 at 09:05