Tutoriales

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) usable. La mayoría de ellos son muy maduros, con código abierto y soporte comercial; otros están vinculados en su mayoría a las bibliotecas de interfaz de usuario C/C++ disponibles. Independientemente, elegir qué biblioteca usar se reduce a tres factores:

  1. madurez: ¿Es estable y bien respaldado por la comunidad, y hay buena documentación?

  2. Integración con Python: Puede pensar que esto es un eufemismo extraño, pero puede ser una gran barrera de entrada para los kits de herramientas (no quiere sentir que está escribiendo una GUI en ensamblador; es Python, después de todo).
  3. ¿Es compatible con su caso de uso? Si principalmente desea escribir tablas, entonces la biblioteca es como Pyforms o Tegente Podría ser mejor para ti. (Tkinker es bien conocido). Si su GUI es más compleja, entonces wxPython Podría ser más apropiado ya que admite una amplia gama de funciones.

Un buen administrador de sistemas debe saber cómo crear aplicaciones fáciles de usar. Se sorprenderá de cuánto pueden mejorar su productividad y la productividad de sus usuarios.

Tienes muchos marcos para elegir. En este artículo, describiré tres de ellos: Rich, Tkinter y DearPyGui.

Desvío rápido: prepare su entorno

Si desea seguir este breve tutorial, ejecute los siguientes comandos para preparar su entorno:

$ 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.?

Estás listo para ir.

Mostrar una lista de RPM ordenados por tamaño

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

$ ./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

También debería permitir a los usuarios volver a ejecutar sus consultas anulando el número de coincidencias y los nombres de los paquetes, así como ordenarlos por tamaño de byte.

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

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

1. rico

Will McGuggan Escribió un marco muy fácil de usar llamado Rich.No ofrece una tonelada de widgets (un proyecto hermano aún en beta llamado texto más orientado a los componentes.Mira esto ejemplo de tabla).

Instalar rico

Instalar marco rico:

$ pip install rich

Aquí está el código para mi script de Python en Rich.producirá una barra de progreso y un resultado verdadero 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 tablas y barras de progreso al script original.

Este es el aspecto de la nueva y mejorada interfaz de usuario de texto.

(José Vicente Nunes, 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 un monton de de Documentación y ejemplosLa documentación allí también es deficiente, por lo que recomendaría seguir con el oficial. tutorial Luego, una vez que haya dominado los conceptos básicos, puede pasar a otros tutoriales que le interesen.

Hay algunas cosas a tener en cuenta aquí:

  • Comprueba si tu sistema tiene Tkinter instalado correctamente como esto: python -m tkinter.
  • Use funciones de devolución de llamada para hacer que su GUI responda a eventos (command=).
  • Tkinter se comunica usando variables especiales que rastrean los cambios (Varme 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 Nunes, CC BY-SA 4.0)

Sin embargo, esto también significa que una vez que se inicia el script, puede volver a ejecutar la consulta ajustando los parámetros en el marco de opciones de búsqueda.

3. Estimado PyGui

Estimado PyGui atravesar jonathan hofstadt Es multiplataforma (Linux, Windows, macOS) y tiene algunas características interesantes.

Instalar DearPyGui

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

$ pip install dearpygui

Aquí está la aplicación reescrita con 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 la creación de GUI. El código también es menos detallado que el código Tkinter y tiene un soporte de tipo mucho mejor (por ejemplo, PyCharm proporciona parámetros de autocompletado para métodos, etc.).

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

DearPyGui aún es joven (versión 1.0.3 en el momento de escribir este artículo) y tiene Algunos erroresespecialmente 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 Nunes, CC BY-SA 4.0)

Más consejos para mejorar las aplicaciones de los usuarios

  • Hay muchas opciones en Python para hacer que sus scripts sean más fáciles de usar.incluso acciones simples como Usando Argparse Tendrá un impacto significativo en cómo se usa el script.

  • Busque documentación oficial y grupos de usuarios. Además, no olvides que hay buenos tutoriales por ahí.
  • Rico y Tegente es una alternativa comprobada para mejorar su interfaz de usuario, y querido PyGUI Parece prometedor.
  • No todo necesita una interfaz de usuario compleja. Sin embargo, los marcos como Rich facilitan la mejora de los programas al hacer que las excepciones y la verificación de objetos sean más legibles en scripts de texto sin formato.

Publicaciones relacionadas

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Botón volver arriba