Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Publicaciones sobre pyqt (publicaciones antiguas, página 13)

PyQt Quickie: parsear línea de comandos

opt_parser = OptionParser()
opt_parser.add_option("-q", dest="quickly", action="store_true",
    help="Do it quickly (default=False)")
(options, args) = opt_parser.parse_args(sys.argv)
app = QApplication(sys.argv)
:
:
:

O tal vez incluso QA­ppli­ca­tio­n([]). Bueno, eso está mal. Y está mal en casi todos los tutoriales, también. ¿Porqué? Porque Qt (y por lo tanto PyQt) soporta un montón de opciones útiles. Al hacerlo como en ese primer listado, si le pasás "-style=oxygen" o lo que sea, va a pasar alguna de estas cosas:

  1. Op­­tPa­r­­ser te va a de­­cir que es una op­­ción in­­vá­­li­­da y abo­r­­tar

  2. Vas a ig­­no­­­rar la op­­ción y no vas a ha­­cer na­­da útil con ella

  3. Vas a te­­ner tu pro­­­pia op­­ción -s­­ty­­le y vas a ha­­cer dos co­­sas

Nin­gu­na de esas op­cio­nes es la idea. La ma­ne­ra co­rrec­ta de ha­cer­lo es és­ta:

opt_parser = OptionParser()
opt_parser.add_option("-q", dest="quickly", action="store_true",
    help="Do it quickly (default=False)")
app = QApplication(sys.argv)
(options, args) = opt_parser.parse_args(app.arguments())
:
:
:

De esta manera, le das a PyQt la oportunidad de procesar las opciones que reconoce y después, vos manejás el resto, porque a app.arguments() ya le sacaron todas las opciones de Qt.

El lado malo es que --help va a ser mas lento, porque tiene que instanciar QApplication al divino botón, y vas a tener opciones no documentadas. Soluciones para ambos problemas se dejan como ejercicio.

Escribir, y qué escribir.

Por otro la­do, es­cri­bí una se­rie muy po­pu­lar de pos­ts, lla­ma­da "P­y­Qt en Ejem­plo­s", que (a­di­vi­nen) lle­va mu­cho tiem­po es­tan­ca­da.

El pro­ble­ma con el li­bro es que tra­té de cu­brir de­ma­sia­do te­rreno. Ter­mi­na­do se­ría un li­bro de 500 pá­gi­na­s, y eso in­clu­ye es­cri­bir me­dia do­ce­na de apps de ejem­plo, al­gu­nas de ellas en áreas en las que no soy ex­per­to.

El pro­ble­ma prin­ci­pal con los pos­ts es que el ejem­plo es pe­do­rro (¡a­pp de TO­DO­s!) y ex­pan­dir­la es abu­rri­do.

¡Qué me­jor ma­ne­ra de re­sol­ver el pro­ble­ma que mez­clar las dos co­sas!

Voy a de­jar Py­thon No Muer­de co­mo es­tá, y voy a ha­cer un li­bro nue­vo, que se lla­me Py­Qt No Muer­de. Va a man­te­ner el tono y el len­gua­je del an­te­rio­r, y va a com­par­tir va­rios ca­pí­tu­lo­s, pe­ro se va a en­fo­car en de­sa­rro­llar apps Py­Q­t, en vez de apun­tar a me­tas de­ma­sia­do am­bi­cio­sas. Es­pe­ro que sea de unas 200 pá­gi­na­s.

Ten­go per­mi­so de la su­pe­rio­ri­dad (mi se­ño­ra) pa­ra tra­ba­jar en es­to un par de ho­ras al día tem­prano a la ma­ña­na. Tal vez avan­ce, tal vez no. Co­mo siem­pre, yo no pro­me­to, ex­pe­ri­men­to.

PyQt Quickie: Que no te lleve el basurero

Qt tie­ne sus me­ca­nis­mos pa­ra crear y eli­mi­nar ob­je­tos (el ár­bol de QOb­jec­ts, smart poin­ter­s, etc.) y Py­Qt usa Py­tho­n, así que tie­ne gar­ba­ge co­llec­tio­n.

Con­si­de­re­mos un ejem­plo sim­ple:

from PyQt4 import QtCore

def finished():
    print "El proceso termino!"
    # Salir de la aplicación
    QtCore.QCoreApplication.instance().quit()

def launch_process():
    # Hacer algo asincrono
    proc = QtCore.QProcess()
    proc.start("/bin/sleep 3")
    # Cuando termine, llamar a finished
    proc.finished.connect(finished)

def main():
    app = QtCore.QCoreApplication([])
    # Lanzar el proceso
    launch_process()
    app.exec_()

main()

Si eje­cu­tás eso, te va a pa­sar es­to:

QProcess: Destroyed while process is still running.
El proceso termino!

Encima el script no termina nunca. ¡Diversión! El problema es que proc está siendo borrado al final de launch_process porque no hay más referencias a él.

És­ta es una me­jor ma­ne­ra de ha­cer­lo:

from PyQt4 import QtCore

processes = set([])

def finished():
    print "El proceso termino!"
    # Salir de la aplicación
    QtCore.QCoreApplication.instance().quit()

def launch_process():
    # Hacer algo asincrono
    proc = QtCore.QProcess()
    processes.add(proc)
    proc.start("/bin/sleep 3")
    # Cuando termine, llamar a finished
    proc.finished.connect(finished)

def main():
    app = QtCore.QCoreApplication([])
    # Lanzar el proceso
    launch_process()
    app.exec_()

main()

Al agregar un processes global y meter ahí proc, mantenemos siempre una referencia, y el programa funciona. Sin embargo, sigue teniendo un problema: nunca eliminamos los objetos QProcess.

Si bien en es­te ca­so la pér­di­da de me­mo­ria es muy bre­ve por­que el pro­gra­ma ter­mi­na en­se­gui­da, en un pro­gra­ma de ver­dad es­to no es bue­na idea.

Así que necesitamos agregar una manera de sacar proc de processes cuando no lo necesitemo. Esto no es tan fácil como parece. Por ejemplo, esto no funciona bien:

def launch_process():
    # Hacer algo asincrono
    proc = QtCore.QProcess()
    processes.add(proc)
    proc.start("/bin/sleep 3")
    # Sacamos el proceso del global cuando no lo necesitamos
    proc.finished.connect(lambda: processes.remove(proc))
    # Cuando termine, llamar a finished
    proc.finished.connect(finished)

¡En esta versión, todavía tenemos un memory leak de proc, aunque processes esté vacío! Lo que pasa es que el lambda contiene una referencia a proc.

No tengo una my buena respuesta para este problema que no involucre convertir todo en miembros de un Qbject y usar sender para saber cuál proceso es el que termina, o usar QSignalMapper. Esa versión la dejo como ejercicio para el lector ;-)

PyQt Quickie: QTimer

El pri­mer ca­so es así:

# llamar f() en 3 segundos
QTimer.singleShot(3000, f)

El se­gun­do es así:

# Creamos un QTimer
timer = QTimer()
# Lo conectamos a f
timer.timeout.connect(f)
# Llamamos a f() cada 5 segundos
timer.start(5000)

¿Fá­ci­l, no? Bue­no, sí, pe­ro tie­ne un par de tram­pa­s.

  1. Hay que guar­dar la re­fe­ren­cia a ti­mer

    Si no, lo re­­co­­­ge el ba­­su­­re­­ro, y nun­­ca se lla­­ma a f()

  2. Ca­­paz que son más de 5 se­­gun­­dos

    Va a lla­mar a f() más o me­nos ca­da 5 se­gun­dos des­pués de que en­tre al event lo­op. Tal vez eso no sea en­se­gui­da des­pués de que arran­cás el ti­me­r!

  3. Ca­­paz que se pi­san las lla­­ma­­das

    Si f() tar­da mu­cho en ter­mi­na­r, y vuel­ve a en­trar al event lo­op (por ejem­plo, lla­man­do a pro­ce­ssE­ven­ts) tal vez ti­mer se dis­pa­re an­tes que f() ter­mi­ne, y la lla­me de nue­vo. Eso ca­si nun­ca es bue­na idea.

Una al­ter­na­ti­va:

def f():
    try:
        # Hacé cosas
    finally:
        QTimer.singleShot(5000, f)

f()

Ese fragmento llama a f() una sola vez, pero ella misma se pone en cola para correr en 5 segundos. Ya que lo hace en un finally lo va a hacer aún si las cosas se rompen.

O sea, no se va a pisar. También quiere decir que no son 5 segundos, sino 5 más lo que tarde f. Y no hace falta guardar referencias al QTimer.

Úl­ti­mo ti­po: po­dés usar QTi­mer pa­ra que al­go se ha­ga "a­pe­nas es­tés en el event lo­op"

QTimer.singleShot(0, f)

¡O­ja­lá sir­va!


Contents © 2000-2023 Roberto Alsina