3 marcos de interfaz de usuario para escribir aplicaciones fáciles de usar en Python

pitón tiene muchos marcos de interfaz gráfica de usuario (GUI) disponible. La mayoría de ellos son muy maduros con código abierto y soporte comercial; otros son principalmente enlaces a las bibliotecas de interfaz de usuario C / C ++ disponibles. En cualquier caso, la elección de qué biblioteca utilizar se reduce a tres factores:

  1. Madurez: ¿Es estable y bien respaldado por la comunidad, y tiene buena documentación?
  2. Integración con Python: Puede pensar que es un eufemismo extraño, pero puede ser una barrera significativa para ingresar a la caja de herramientas (no querrá sentir que está escribiendo una GUI en un ensamblador; después de todo, esto es Python).
  3. ¿Es compatible con su caso de uso? Si principalmente desea escribir formularios, entonces bibliotecas como Pyforms Donde Tkinter puede ser mejor para ti. (Tkinker es bien conocido). Si su GUI es más compleja, entonces wxPython puede ser una mejor opción porque admite una amplia gama de funciones.

Un buen administrador de sistemas debe saber cómo crear aplicaciones fáciles de usar. Te sorprenderá lo mucho que pueden mejorar tu productividad y la de tus usuarios.

Tienes la opción entre varios marcos. En este artículo, daré una descripción general de tres de ellos: Rich, Tkinter y DearPyGui.

Índice

Un pequeño desvío: prepara tu entorno

Si desea seguir este breve tutorial, prepare su entorno ejecutando:

$ git clone https://github.com/josevnz/rpm_query
$ cd rpm_query
$ python3 -m venv --system-site-packages ~/virtualenv/rpm_query
$ . ~/virtualenv/rpm_query/bin/activate
$ python3 setup.py build
$ cp reporter build/scripts-3.?

Estas listo.

Mostrar la lista de RPM ordenados por tamaño

Esta aplicación de muestra no es muy compleja. Debería mostrar el siguiente resultado:

$ ./rpmq_simple.py --limit 10
linux-firmware-20210818: 395,099,476
code-1.61.2: 303,882,220
brave-browser-1.31.87: 293,857,731
libreoffice-core-7.0.6.2: 287,370,064
thunderbird-91.1.0: 271,239,962
firefox-92.0: 266,349,777
glibc-all-langpacks-2.32: 227,552,812
mysql-workbench-community-8.0.23: 190,641,403
java-11-openjdk-headless-11.0.13.0.8: 179,469,639
iwl7260-firmware-25.30.13.0: 148,167,043

Esto también debería permitir al usuario volver a ejecutar su consulta mientras reemplaza la cantidad de coincidencias y nombres de paquetes, así como clasificarlos por tamaño en bytes.

[ Sign up for the free online course Red Hat Enterprise Linux Technical Overview. ]

Ahora que todo está configurado, puede comenzar a crear una aplicación. Aquí hay tres marcos a considerar.

1. rico

mcgugan escribió un marco increíblemente fácil de usar llamado Rich. No ofrece toneladas de widgets (un proyecto hermano aún en beta llamado Textual está más orientado a los componentes. Mira esto tabla de ejemplo).

Instalar rico

Instale el marco rico:

$ pip install rich

Aquí está el código para mi secuencia de comandos Python Rich. Produce una barra de progreso y resultados en un De Verdad bonita mesa:

#!/usr/bin/env python
"""
# rpmq_rich.py - A simple CLI to query the sizes of RPM on your system
Author: Jose Vicente Nunez
"""
import argparse
import textwrap
from reporter import __is_valid_limit__
from reporter.rpm_query import QueryHelper
from rich.table import Table
from rich.progress import Progress

if __name__ == "__main__":

    parser = argparse.ArgumentParser(description=textwrap.dedent(__doc__))
    parser.add_argument(
        "--limit",
        type=__is_valid_limit__,  # Custom limit validator
        action="store",
        default=QueryHelper.MAX_NUMBER_OF_RESULTS,
        help="By default results are unlimited but you can cap the results"
    )
    parser.add_argument(
        "--name",
        type=str,
        action="store",
        help="You can filter by a package name."
    )
    parser.add_argument(
        "--sort",
        action="store_false",
        help="Sorted results are enabled bu default, but you fan turn it off"
    )
    args = parser.parse_args()

    with QueryHelper(
            name=args.name,
            limit=args.limit,
            sorted_val=args.sort
    ) as rpm_query:
        rpm_table = Table(title="RPM package name and sizes")
        rpm_table.add_column("Name", justify="right", style="cyan", no_wrap=True)
        rpm_table.add_column("Size (bytes)", justify="right", style="green")
        with Progress(transient=True) as progress:
            querying_task = progress.add_task("[red]RPM query...", start=False)
            current = 0
            for package in rpm_query:
                if current >= args.limit:
                    break
                rpm_table.add_row(f"{package['name']}-{package['version']}", f"{package['size']:,.0f}")
                progress.console.print(f"[yellow]Processed package: [green]{package['name']}-{package['version']}")
                current += 1
            progress.update(querying_task, advance=100.0)
            progress.console.print(rpm_table)

Es sorprendente lo fácil que es agregar una tabla y una barra de progreso al script original.

Así es como se ve la nueva y mejorada interfaz de usuario de texto.

(José Vicente Núñez, CC BY-SA 4.0)

2. Tkinter

Tkinter es una colección de marcos: TCL, TK y widgets (Ttk).

El marco es maduro, y tiene muchos de documentacion y ejemplos. También hay mala documentación, así que te sugiero que te ciñas a la oficial. Tutorial luego, una vez que haya dominado los conceptos básicos, continúe con otros tutoriales que le interesen.

Aquí hay algunas cosas a tener en cuenta:

  • Compruebe si Tkinter está correctamente instalado en su sistema así: python -m tkinter.
  • Haga que su GUI responda a los eventos mediante el uso de funciones de devolución de llamada (command=).
  • Tkinter se comunica utilizando variables especiales que rastrean los cambios por usted (Var, Me gusta StringVar).

¿Cómo se ve el código en Tkinter?

#!/usr/bin/env python
"""
# rpmq_tkinter.py - A simple CLI to query the sizes of RPM on your system
This example is more complex because:
 * Uses callbacks (commands) to update the GUI and also deals
 * Deals with the placement of components using a frame with Grid and a flow layout
Author: Jose Vicente Nunez
"""
import argparse
import textwrap
from tkinter import *
from tkinter.ttk import *
from reporter import __is_valid_limit__
from reporter.rpm_query import QueryHelper


def __initial__search__(*, window: Tk, name: str, limit: int, sort: bool, table: Treeview) -> NONE:
    """
    Populate the table with an initial search using CLI args
    :param window:
    :param name:
    :param limit:
    :param sort:
    :param table:
    :return:
    """
    with QueryHelper(name=name, limit=limit, sorted_val=sort) as rpm_query:
        row_id = 0
        for package in rpm_query:
            if row_id >= limit:
                break
            package_name = f"{package['name']}-{package['version']}"
            package_size = f"{package['size']:,.0f}"
            table.insert(
                parent="",
                index='end',
                iid=row_id,
                text="",
                values=(package_name, package_size)
            )
            window.update()  # Update the UI as soon we get results
            row_id += 1


def __create_table__(main_w: Tk) -> Treeview:
    """
    * Create a table using a tree component, with scrolls on both sides (vertical, horizontal)
    * Let the UI 'pack' or arrange the components, not using a grid here
    * The table reacts to the actions and values of the components defined on the filtering components.
    :param main_w
    """
    scroll_y = Scrollbar(main_w)
    scroll_y.pack(side=RIGHT, fill=Y)
    scroll_x = Scrollbar(main_w, orient="horizontal")
    scroll_x.pack(side=BOTTOM, fill=X)
    tree = Treeview(main_w, yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
    tree.pack()
    scroll_y.config(command=tree.yview)
    scroll_x.config(command=tree.xview)
    tree['columns'] = ('package_name', 'package_size')
    tree.column("#0", width=0, stretch=NO)
    tree.column("package_name", anchor=CENTER, width=500)
    tree.column("package_size", anchor=CENTER, width=100)
    tree.heading("#0", text="", anchor=CENTER)
    tree.heading("package_name", text="Name", anchor=CENTER)
    tree.heading("package_size", text="Size (bytes)", anchor=CENTER)
    return tree


def __cli_args__() -> argparse.Namespace:
    """
    Command line argument parsing
    :return:
    """
    parser = argparse.ArgumentParser(description=textwrap.dedent(__doc__))
    parser.add_argument(
        "--limit",
        type=__is_valid_limit__,  # Custom limit validator
        action="store",
        default=QueryHelper.MAX_NUMBER_OF_RESULTS,
        help="By default results are unlimited but you can cap the results"
    )
    parser.add_argument(
        "--name",
        type=str,
        action="store",
        default="",
        help="You can filter by a package name."
    )
    parser.add_argument(
        "--sort",
        action="store_false",
        help="Sorted results are enabled bu default, but you fan turn it off"
    )
    return parser.parse_args()


def __reset_command__() -> None:
    """
    Callback to reset the UI form filters
    Doesn't trigger a new search. This is on purpose!
    :return:
    """
    query_v.set(args.name)
    limit_v.set(args.limit)
    sort_v.set(args.sort)


def __ui_search__() -> None:
    """
    Re-do a search using UI filter settings
    :return:
    """
    for i in results_tbl.get_children():
        results_tbl.delete(i)
        win.update()
    __initial__search__(
        window=win, name=query_v.get(), limit=limit_v.get(), sort=sort_v.get(), table=results_tbl)


def test(arg):
    print(arg)


if __name__ == "__main__":
    args = __cli_args__()
    win = Tk()
    win.title("RPM Search results")
    # Search frame with filtering options. Force placement using a grid
    search_f = LabelFrame(text="Search options:", labelanchor=N, relief=FLAT, padding=1)
    query_v = StringVar(value=args.name)
    query_e = Entry(search_f, textvariable=query_v, width=25)
    limit_v = IntVar(value=args.limit)
    limit_l = Label(search_f, text="Limit results: ")
    query_l = Spinbox(
        search_f,
        from_=1,  # from_ is not a typo and is annoying!
        to=QueryHelper.MAX_NUMBER_OF_RESULTS,
        textvariable=limit_v
    )
    sort_v = BooleanVar(value=args.sort)
    sort_c = Checkbutton(search_f, text="Sort by size", variable=sort_v)
    search_btn = Button(search_f, text="Search RPM", command=__ui_search__)
    clear_btn = Button(search_f, text="Reset filters", command=__reset_command__)
    package_l = Label(search_f, text="Package name: ").grid(row=0, column=0, sticky=W)
    search_f.grid(column=0, row=0, columnspan=3, rowspan=4)
    limit_l.grid(row=1, column=0, sticky=W)
    query_e.grid(row=0, column=1, columnspan=2, sticky=W)
    query_l.grid(row=1, column=1, columnspan=1, sticky=W)
    sort_c.grid(row=2, column=0, columnspan=1, sticky=W)
    search_btn.grid(row=3, column=0, columnspan=2, sticky=W)
    clear_btn.grid(row=3, column=1, columnspan=1, sticky=W)
    search_f.pack(side=TOP, fill=BOTH, expand=1)
    results_tbl = __create_table__(win)
    results_tbl.pack(side=BOTTOM, fill=BOTH, expand=1)
    __initial__search__(
        window=win, name=query_v.get(), limit=limit_v.get(), sort=sort_v.get(), table=results_tbl)
    win.mainloop()

El código es más detallado, principalmente debido al manejo de eventos.

(José Vicente Núñez, CC BY-SA 4.0)

Sin embargo, también significa que puede volver a ejecutar sus consultas después de iniciar el script ajustando los parámetros en las opciones de búsqueda.

3. Estimado PyGui

Estimado PyGui mediante jonathan hoffstadt es multiplataforma (Linux, Windows, macOS) y tiene grandes capacidades.

Instalar DearPyGui

Si tiene un sistema actual (como Fedora 33 o Windows 10 Pro), la instalación debería ser bastante sencilla:

$ pip install dearpygui

Aquí está la aplicación reescrita en DearPyGui:

#!/usr/bin/env python
"""
# rpmq_dearpygui.py - A simple CLI to query the sizes of RPM on your system
Author: Jose Vicente Nunez
"""
import argparse
import textwrap

from reporter import __is_valid_limit__
from reporter.rpm_query import QueryHelper
import dearpygui.dearpygui as dpg

TABLE_TAG = "query_table"
MAIN_WINDOW_TAG = "main_window"


def __cli_args__() -> argparse.Namespace:
    """
    Command line argument parsing
    :return:
    """
    parser = argparse.ArgumentParser(description=textwrap.dedent(__doc__))
    parser.add_argument(
        "--limit",
        type=__is_valid_limit__,  # Custom limit validator
        action="store",
        default=QueryHelper.MAX_NUMBER_OF_RESULTS,
        help="By default results are unlimited but you can cap the results"
    )
    parser.add_argument(
        "--name",
        type=str,
        action="store",
        default="",
        help="You can filter by a package name."
    )
    parser.add_argument(
        "--sort",
        action="store_false",
        help="Sorted results are enabled bu default, but you fan turn it off"
    )
    return parser.parse_args()


def __reset_form__():
    dpg.set_value("package_name", args.name)
    dpg.set_value("limit_text", args.limit)
    dpg.set_value("sort_by_size", args.sort)


def __run_initial_query__(
        *,
        package: str,
        limit: int,
        sorted_elem: bool
) -> None:
    """
    Need to ensure the table gets removed.
    See issue: https://github.com/hoffstadt/DearPyGui/issues/1350
    :return:
    """
    if dpg.does_alias_exist(TABLE_TAG):
        dpg.delete_item(TABLE_TAG, children_only=False)
    if dpg.does_alias_exist(TABLE_TAG):
        dpg.remove_alias(TABLE_TAG)
    with dpg.table(header_row=True, resizable=True, tag=TABLE_TAG, parent=MAIN_WINDOW_TAG):
        dpg.add_table_column(label="Name", parent=TABLE_TAG)
        dpg.add_table_column(label="Size (bytes)", default_sort=True, parent=TABLE_TAG)
        with QueryHelper(
                name=package,
                limit=limit,
                sorted_val=sorted_elem
        ) as rpm_query:
            current = 0
            for package in rpm_query:
                if current >= args.limit:
                    break
                with dpg.table_row(parent=TABLE_TAG):
                    dpg.add_text(f"{package['name']}-{package['version']}")
                    dpg.add_text(f"{package['size']:,.0f}")
                current += 1


def __run__query__() -> None:
    __run_initial_query__(
        package=dpg.get_value("package_name"),
        limit=dpg.get_value("limit_text"),
        sorted_elem=dpg.get_value("sort_by_size")
    )


if __name__ == "__main__":

    args = __cli_args__()

    dpg.create_context()
    with dpg.window(label="RPM Search results", tag=MAIN_WINDOW_TAG):
        dpg.add_text("Run a new search")
        dpg.add_input_text(label="Package name", tag="package_name", default_value=args.name)
        with dpg.tooltip("package_name"):
            dpg.add_text("Leave empty to search all packages")
        dpg.add_checkbox(label="Sort by size", tag="sort_by_size", default_value=args.sort)
        dpg.add_slider_int(
            label="Limit",
            default_value=args.limit,
            tag="limit_text",
            max_value=QueryHelper.MAX_NUMBER_OF_RESULTS
        )
        with dpg.tooltip("limit_text"):
            dpg.add_text(f"Limit to {QueryHelper.MAX_NUMBER_OF_RESULTS} number of results")
        with dpg.group(horizontal=True):
            dpg.add_button(label="Search", tag="search", callback=__run__query__)
            with dpg.tooltip("search"):
                dpg.add_text("Click here to search RPM")
            dpg.add_button(label="Reset", tag="reset", callback=__reset_form__)
            with dpg.tooltip("reset"):
                dpg.add_text("Reset search filters")
        __run_initial_query__(
            package=args.name,
            limit=args.limit,
            sorted_elem=args.sort
        )

    dpg.create_viewport(title="RPM Quick query tool")
    dpg.setup_dearpygui()
    dpg.show_viewport()
    dpg.start_dearpygui()
    dpg.destroy_context()

Tenga en cuenta que DearPyGui usa contextos al anidar componentes, lo que facilita enormemente la creación de la GUI. El código también es menos detallado que el código Tkinter, y el soporte de tipos es mucho mejor (por ejemplo, PyCharm ofrece ingresar argumentos de método automáticamente, etc.).

[ Learn how IT modernization works on multiple levels to eliminate technical debt in both time and money. Download Alleviate technical debt. ]

DearPyGui es todavía muy joven (versión 1.0.3 en el momento de escribir este artículo) y tiene algunos errores, especialmente en distribuciones antiguas de Linux, pero parece muy prometedor y está en desarrollo activo.

Entonces, ¿cómo se ve la interfaz de usuario en DearPyGui?

(José Vicente Núñez, CC BY-SA 4.0)

Más consejos para mejorar tus aplicaciones de usuario

  • Tiene muchas opciones en Python para hacer que sus scripts sean más fáciles de usar. Incluso acciones simples como usando Argparse tendrá un gran impacto en cómo se usa un script.
  • Busque documentación oficial y grupos de usuarios. Además, no olvide que hay algunos buenos tutoriales por ahí.
  • Rico y Tkinter son alternativas maduras para hacer que su interfaz de usuario sea mucho mejor, y Estimado PyGUI parece muy prometedor.
  • No todo necesita una interfaz de usuario compleja. Pero los marcos como Rich facilitan la mejora de sus programas al hacer que las inspecciones de objetos y excepciones sean más legibles en sus scripts de solo texto.


Si quieres conocer otros artículos similares a 3 marcos de interfaz de usuario para escribir aplicaciones fáciles de usar en Python puedes visitar la categoría Programación.

Artículos de interés

Subir

Si continuas utilizando este sitio aceptas el uso de cookies. Más información