Αντρω δι Νεξυνω

[Python] GUI & PDF a passetti piccoli piccoli

« Older   Newer »
  Share  
view post Posted on 2/5/2021, 07:47
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


I miei saluti ad improbabili lettori.

Il recente coinvolgimento in una discussione, da cui è scaturito questo post, mi ha un po' stimolato.

In effetti manipolare files PDF per varie finalità è cosa frequente nel mio ambito lavorativo, anche se in genere li manipolo direttamente da riga di comando della shell linux, s.o. che fornisce un sacco di tools in materia, ho realizzato numerosi script bash per la manipolazione di tali files, tanto in modalità singola quanto in modalità massiva.
Nel corso della discussione sopra accennata, ad una richiesta dello OP indicai allo stesso la praticità di script "ad-och" rispetto alla complessità necessaria per far eseguire certe impostazioni ad un user ... in effetti è così, la linea di comando è enormemente più efficiente di una interfaccia grafica.

... d'altro canto, ciò che si fa con uno script può essere fatto anche tramite una GUI, affrontare "la complessità" è sempre un buon esercizio ed il mio studio del python langue, il poco tempo disponibile e l'assenza del "dover fare" mi portano a dimenticare cio che studio, provare ad unificare le cosette faccio per lavoro in una interfaccia grafica può essere un utile esercizio e postare qui i vari punti che si affrontano può essere utile a me per fissare le idee, oltre che annotare eventuali particolarità affrontate (incfredibile quanto dimentico in fretta), potrebbe essere utile a qualcuno che ne sappia meno di me e, inifine, potrebbe essere uno spunto di discussione (anche se dubito che qualcuno leggera mai questi scritti).

Come si svolgeranno questi post


Come detto, questo è un esercizio di studio, non ha indirizzi precisi ne' uno sviluppo ben definito, mi porrò una problematica, l'affronterò ed una volta risolta posterò la soluzione che potrà variare nel prosieguo per nuove esigenze o soluzioni.

La finalità non è risolvere i PDF di per se ma affrontare la programmazione di interfacce grafiche, mio interesse specifico è il framework wxPython, sul quale ho notevoli punti oscuri malgrado utilizzi l'eccellente testo di Riccardo Poligneri, essendo un framework molto maturo, complesso ed articolato.
Per altro, so per averlo visto quasi quotidianamente, che la maggior parte di coloro che iniziano con python cercano immediatamente di utilizzare tkinter (altro framework grafico), magari senza avere il minimo concetto di programmazione ad oggetti, non è che io sia un drago con tkinter ma qualche cosa mi riesce di farla, ad uso degli improbabili apprendisti realizzerò una doppia versione di ogni singolo step, prima in tkinter e poi con wxPython che saranno postate separatamente ... ovviamente, caso mai qualcuno volesse proporre proprie versioni con altri framework (tipo QT, etc.) cerchi di contattarmi, se ci riesce lo ammetterò a scrivere qui.

Nel caso a qualcuno interessasse provare gli script


È mia abitudine utilizzare i virtual-environment (venv per gli amici :D ) specifici per ciò che intendo fare, ciò tanto al fine di non "sporcare" il sistema quanto per avere un ambiente specifico per una eventuale package della realizzazione.
per il primo passo, che sarà scritto di seguito a questo post, occorrerà la libreria "pdf2image"* e nel caso interessi vedere wxPython anche quest'ultima libreria, questo è lo stato, al momento del venv dedicato (ambiente Linux)

CODICE
NzP:~$ cd py_venvs/pdf_v
NzP:~$ . bin/activate
(pdf_v) NzP:~$ python -m pip list
Package       Version
------------- -------
numpy         1.20.2
pdf2image     1.14.0
pdftotext     2.1.5
Pillow        8.2.0
pip           21.0.1
pkg-resources 0.0.0
setuptools    44.0.0
six           1.15.0
wxPython      4.1.1

Nel venv è presente anche la libreria "pdftotext", utilizzata per lo sviluppo nel post lincato all'inizio ed al momento ridondante.

Se non sapete come si definisce un venv o si installa una libreria cercate in rete la documentazione relativa al vostro sistema operativo, vi sarà utile apprendimento.

* pdf2image prevede sia installata nel sistema la libreria poppler, vedere nella documentazione cosa fare secondo il Vostro sistema operativo

Il primo passetto


Nella mia esperienza (da burocrate) il grosso delle manipolazioni di file pdf effettuate riguardano pdf derivati da scansione, immagini in sostanza, riportanti timbri, firme, etc, in genere sufficientemente organizzati da permettere operazioni massive di estrazione testo tramite ocr, per copiarlo oppure per effettuare ricerche nei testi derivati (faccenda molto frequente).
Non di meno, è anche frequente vi siano documenti scanditi in verticale quando dovrebbero essere in orizzontale od anche capovolti, condizione che costringe a trattare singolarmente i files ruotando le pagine "a posizione impropria" per poi trattarle.

La considerazione su esposta evidenzia una necessità preliminare, cioè disporre di un "visualizzatore di documenti pdf" che permetta di visualizzare le singole pagine di un documento per poi decidere cosa fare.

La realizzazione di un rudimentale visualizzatore, con capacità di zoom e rotazione pagine, tanto con tkinter quanto con le wx, sarà l'obiettivo dei prossimi due post.

Edited by nuzzopippo - 6/5/2021, 08:38
 
Web  Top
view post Posted on 4/5/2021, 16:18
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


Prototipo visualizzatore PDF in tkinter: preliminari


Come esposto nella introduzione, uno degli strumenti che molto probabilmente servirebbe in una gestione generica di files pdf è un visualizzatore.

Realizzare un prototipo di basso profilo ma funzionale di un tale strumento è, tutto sommato, piuttosto semplice, nelle librerie Python vi sono molte specializzate sui pdf, alcune estremamente specifiche a determinati compiti
... certo, tal volta accavallano le loro funzionalità e non è detto che una scelta effettuata in un primo passo sia valida in seguito, questi primi post sono "esplorativi".

Come anticipato, in questo esempio utilizzeremo, oltre a tkinter, la libreria "pdf2img", tale libreria permette, tra l'altro, di estrarre informazioni relative ad file pdf assegnato e di produrre immagini delle pagine tanto in memoria, quanto su file.
nell'esempio viene utilizzata, inoltre, la versione per consultazione del testo "ReportLab - PDF Processing with Python", per produrre le immagini.

png



Ovviamente potrà essere utilizzato un pdf a piacere, purché se ne cambi il riferimento nel codice che verrà esposto.

Accortezze


Questo è un prototipo per test di fattibilità; è destinato a far parte di un contesto applicativo più ampio. Non sono disposti, pertanto, tutta una serie di accorgimenti, al momento il pdf di esempio deve risiedere nella direttrice dello script, così come devono esistere nella stessa direttrice una cartella "my_tmp", contenente a sua volta una cartella "pdfcache", dedicata al temporaneo storage delle immagini prodotte.

In particolare, non sono al momento considerate eventuali condizioni di errore, che verranno articolate in un secondo momento, quando si cercherà di trasformare il visualizzatore in una utilità di manipolazione a singolo documento.

Alcuni particolari "costruttivi" preliminari


Come si può vedere dalla immagine prima esposta, al momento viene utilizzato un sistema di comando "a bottoni" (cambierà), tali pulsanti di comando non sono forniti direttamente di testo descrittivo bensì di una serie di icone tendenti alla loro "funzione", qualcosa tipo "ideogrammi" insomma ;) , oltre che forniti di tooltip descriventi la loro funzione :

png


Vedremo ora delle "particolarità" in merito

Sulle "icone"


Ho una particolarissima avversione a condire il mio codice con miriadi di file per risorse varie, tipo immagini, testi, etc.
Per mediare tale mia avversione ai file "esterni" al mio codice al piacere che mi da il creare pulsanti coloratissimi (la vita è così grigia di per se) ho preso da tempo l'abitudine di serializzare le risorse che voglio utilizzare, nel caso specifico tali serializzazioni sono contenute nel file "viewer_ico_and_tip.py", che seguirà in calce a questo sub-capitolo.
Dette risorse sono, in sostanza, delle immagini codificate su base64 associate ad una chiave di dizionario assieme ad una stringa rappresentante un "tooltip" e restituite da un oggetto, "IcoDispencer", in base alla chiave richiesta.
... Ovviamente, non è che mi costruisca "a manina" tali serializzazioni, il mio primo tentativo di programmazione in tkinter è stato proprio una utilità per creare tal tipo di moduli risorse python, è rudimentale ma se interessa potete trovarlo qua, altrimenti utilizzare metodi a Voi più consoni.

codice di viewer_ico_and_tip.py:
CODICE
# -*- coding: utf-8 -*-

icoinfo = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFn0lEQVR42s2XA5QjSRjH1zrbeDibD2f
bvls9r22Ogk46zrxMDde2bds7WI2TQU/S3dMZ131fXnV2s0lnNYd67xd++FfVv9WGUvqf8v8VkJCQcK
/Naf7YZDH9wnEpfxnNuu7x4HjDX7yN/9nisHyk1+vvuiEBViv3enZ2xh++Kl/3pqam7pTSqwFi67uXl
ZV1z8hO+8PpNLxyXQIMBsPjs2ZN+QMbB4Nyd0EQgKqrIhAQQjmYOx1q8Dz/2DULcLgtX8pyAAtFNcg9
B7NbV9A9Ye7x7hPTV/ezEcLZnDY3Z9KRK2Ey69N4u4m3OvjBOElNAbjfOAOcjSBEsuFIcfd1Bwu7z1k
wZ9KUKVkeX6WPQCyBvKsBYhuI1+slGVlpbpuN+zGmADQUvIOAQEy2bNsydPbcGZ7m5mYSDNYSv18ggl
B9VYiiEMrB3Bkzp7g58JqmAFmWY5KeSaySJEIhKapB3nkvyVqfTxLmnCBjph8h7lV5ZMuR4qg4zBWhh
t3JT9AUAB6ICe4lzkAU/Tj7CDZBs7UHC8m5Eh8p9FaShTvPkeHZh0l+sS8iDnOxBnjDc7GxMeVtMMgm
3moSjSZdC0DjwZn11JVqV6ZOzy4+dvzYIlEUyeV4KwUyFASs3l8Y9R/0DJkz1NxmN9typmQoFZU+2tj
YSK9yQGwD9XrLadaU9MalyxcfliSJqAh+kbhW5JGUBSeIr9of/p0RFoBL/vvMWdOCsCRUUWqp319D4Z
C7KgKBmlAO5s6aM71x565tG9BkgigR69LTIXyCiMaLIizA5uBBuQiFZFpTUx1B7oVyCqaiiXOOUzAVT
V2ZS7ccLYqKw1xRCtDMbFIVDAbJ+sMlZNzMYyEh8D0mYQGwl40tLS0wGz/OPoJNR4oomIqeLamgheWV
FExFh+ccpvlFvog4zMUaJt7QXFdXR+btPE8MC08S+KxJWAAaCocoBq5IeaVAh4GA1QcKo/7DgbXwZCP
BDP1SkMBnLaIFyLIUFzAVda3IpSnzT1AwVdT/TAD4oYnYluURDlYAPmvCBHhUAWgmTWoCMrUsPR3CJw
RixlwU0Iz7Hzr84HNMsDkeijabOSUsAIyhybrDpRSK0hpR1oqJEBAHdk1oInA6dyckjH8zLKC+vk6Te
TsuUDBV3BgcrFZcAecLz3vsTqtLp5v0WZs2bTqhAPWkoomkBKlfUuLGqAIWLJrvbmhoILGE4OHHW426
b7/99j5o3gXoGDoMMRmMoYlteS41LjoVNwYH1GoaOLDvxxYr58jNPeVBEZcKQWF2J5/ImncOCXC4rEc
URcHjWBPYfzz8tP5nh7FILVbTOSh61zvvvPOA0ajvmzMlyy3VShEirHaTVW0OdMAbkF5z581WoFQ8EX
Gbg6ngVDwjOGbMyN5Q9HbgFuCmISP7vWy2GM379u1JVQ3IWznPc8891wmbA+2ANu1hyVzTpucocK1GF
9PLRzwB5y+cbwJT1SYkjR8Hte4AbsPmQBfkqaeeukVn1P3uTnO6q6srCYG7ob59+2JcO7wQhgQAncaN
G/WRxW7eyttMVerlmJlKgb2LKQS3DmZ07ptvvnkam6szZ0vcidERGT58+GMWmzkBz34pfEr4ZhVHOxb
UhSXfyordDdw3cuSQ/tBEzMs73Xj5iqAwu9NymuXcDHRlzdsz2jHaIpDaluN0H+gt+icjbkrVVWAFsN
Btl4i45/3333/ayOkWT5mapci1coQIMFWV2pzVaHfNT0ZMYQemXhVxKzMUCrkT3T1k+IDvYTXK9+7bU
68aEL43Pvjgg93UmV/fo1mkiE5sO7oxIbegGJUXX3zxXr1J50z1uJSqqkqanuVRBg4c+LDa/HoFaAnp
zMR0RUEI+9xlzJjhr1nt5pNoVANveK3VH06ZkHbMHx3CJ46L5mqbkJDQDgQMNJiT379eAX8D/yxsjEx
hE6MAAAAASUVORK5CYII=
'''

icopg_first = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATRSURBVHja7Z
dbbBRVHMa//+yZnd2ZLkW27ZbStNweClJYQCoJCZBAAC0CklKCIcHEBwkJDyZGNEok8oAJvuHtUdQnH
7Q2xgeJl0feNMZEExFNJMASZLe7O9vZmTnnOLdw0i6XrG1iYtyTL5OZ7+z5fvv/n92cJSkl/s2XFui/
AUBPEqPnKdWGP38AtJd2g+H348Xjf9B4awjta/HnD4CeolNm1pw4/ezp/kaz0Q8PC2b7mXRm4uS+k/f
02T8OLlIaA/hwsDA4fuLICbK6LJz94CyQmun3LeobPzZ6jLILsrjw/gWAgDkD0CEaxAAmRlaNFMf2jk
HqEjW7pvz9tAyD+LS4vFjcs3UPOHHU7TpgAHDnBJD0E7h4YOuBwsjmEdjchpgW4IIj+XQ7kcLbOzft7
Fm7ai1szwb3OYjTXABUP420cebo6NFMz0APyo0yhBTgkkMIEQUYhvHR6JZRI9+VR2W6AuGLSJrQYoBa
2wCqn4VHCuMHnzhIrIOh6lRVeHCVUqKruws7Ht9hsDRDrVmD4IHv8giACRYDoC2ApN9LMbFyYGVx++b
tcHUXTtOJ+iogovBoCIFtW7ahGYxpbzoO9zmEJyLpQm8fgJ6m3dBxcd2qdYXh1cOwhQ3hCvBgCBIKIh
yqFWH4XQDuxRAGjPYA6DC9zCz99Y0rNmSW9C1BzamBpziEJkLFAFoIkoQjDudiBkR8z2NI0jVIiBaA1
n4P0cemZY1tGFxPVs6KwxmHYEloSsTCvauQ7IsYRMSVICKQTpAPqgA9Q0uxhia7OvLDQ91DIKIoXOhC
LcZUIEdYkQSCRKLkOZKW8Ph9TGMq7V4A4W90T2/hu269e7BDWGi6zRm7PBySJIgRpCajCkgmIwBoiDw
pAwl5916TGggUXRtuA5QODPD7V2DX2l3Z0o0Svrn0NWRWAiaQyqVAJoEsgjAEREaAGxzIIN5U6WQVLR
AFklGGkheLeQxkavevgPxE8p4Xe7bcrt/53Bw2V/s3XDQpqkIyIQlhSagBBaEHSiUAIpAfyE3my/gZS
QJ0evB54NZbt67Iy3y9LeqTtILJjnwuXDgWS6SHALMgsoFMpeg+AzVPR7QBSdMefiCRP0hXvif2O/b0
G84i10UvhbNiKRgFYkABWOF1VjiDErVxIpLv8jN+1T8E0u7A1KAzPVpAwcxqhzkrXIcC1hB/DbWHtKA
Vwp9ElW9CSvulw+qEpVvKpFBqX7AMg8lMIK1CofJiANLaPxPKd+RV/OQXy9XylyR1uUjPAwJKPJYudG
QudyBXW6CeCyUSIUB7FVAQk7Ipz/mj9Ur1nD3leH2sH+QR4CW73QHIJjgNB873nofrWujF8kPNDUCBv
MlfbZadwzdKpcoSPgDDNYBpADZA9bjE3jX3uQBgCjdT0F09BAwVAZNsvwWtEOf9z2TZ23itdP3KwmoX
OpudQB2gGiEc+JV/gbL/GBrab51eHjmRA5oAufNQAQUhr+Kyt6ZUKX1Fd9Ky11sMLQaI/bPyCr71Hr1
dv31JY4YsZHuhhQByfgDUvnjF212ZKp+v/FX3lqWWI58xZ/oveLumapXzU6btLesPfMOcNwAV9Jp3yq
k1jvx48+fS4oXGn9AxNcN/KfCrgS8CP9fqz+ufUyIK15Pt+P//O/4b3OWd5QW/m80AAAAASUVORK5CY
II=
'''

icopg_last = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATzSURBVHja7V
dbb1RVGF3f3ufMmXamlHIbLkotjQkV1EmMgA/eSBXSSBlISnkwqPAgCU88qTERjQ+Q9FXxB5gYrxExM
VEf5AnhQSzRmEYqgXBLQ6dzPTNz5lw+9+xzkpmmF7lMog+u05Xv7L2mWet8357JDDEz/k0Ixf9WAHqd
JA2RgQiL6O0PQPtI4hbGd27ZeZOGaQcUFtRfUnrbO+BiCYDNw08Pr4yb8VO0i96YT98/tH9lZ0en1ts
bINrJeTkcHTkaX9uz9rjqxKeUphhaUOmo4NjhY/HeVG9Tb1sACygUC/AtHwcyByjdnx5FL87TbupDhE
KpACSBI68eoS0DW0axXukj1Nu2AHbVRrlShu3Z2P7MdgxuHUxD4hyAQRBQqpZQrio9sJHZlUHm2UwaD
s7rc9OWADW7YaCN8tU8+h7uw94X9q6yktbHDb3sKK2mQtTKyFVzGEgP4NCeQykrZulz0Y4A2qDklEIz
VTuWdiAzmLFWrFwBu26H+5FerBXRmerEwZGD8dSy1F2di1mfhLSHerABM0NrhuAKFyImIEwBaUoIQ91
LdQ8ZrqVakyLCKlnqe3IJZ86eweTVyXGYyPAXfBWLwJi3A54NhxwIigxJIrSKQvhqHah7oYjwNYJ11f
vbtm5DIplIX7x08bx6qFf4a/7+jgOQKVD1qqigog2klBB+ZOZrk9BURPuInp5FyECtA4n+Df2Ix+OpX
/68cIpG6T3+jE/c0QjEE3Jmk/8IilyEjEsISzRHYUhNHYZmP70INMPu+FH1BOyijQuXf+WKXfkSE/wy
j3N98QBb5Uxf7SHk/TxEh9ABpCXDaujZh6SW1gfaVFN6obFwo7UrwS5j4tYEpovZ3xDwMH/CVxYegSF
Q8SowYgbIJE02GDChK0vWtYFAXY0/9lkHIZ9QD+ogj8JAdR1AB+pf0o9VydSj2Vr2DO2jfv6c/fkDCM
Lt+m146oIEIBBWF00EihxVT7Gu6ADSUWaO0OQKa/plH7ABqpI6nE/hucefT5z85oPFDiEBmgBiipZiP
Kqx6D9EFMCPgglo+IEP3/MxCwRYCQvGgzGcnfr5j6vOld366RcMIAQQtjwKodkMYbYE8BRlS1f8aM/Q
1FqypwtePGC7VP4W53jk2vi1+uKfA6QoIxqac0PIyNBVpJZRuFprjm45oVav172ie5w/Ct7FHCxwBtA
gNeevabaEMCPTWjNIZKppGiZc6QMuzXhF7zU+6Z++869kJEAgtKI1TKfohJEwAKvlPJCmRsJIICm6AR
YTKPpP8klPm99FAAIxAQFm0we6qksQv5CE6Zl6HbL5mmViOcg2OZfPfYffvTR/yJfxDzDmCwCPwmhey
2yzArUZ1/UBk8oU7jnhW5Bcwhp/HbJTOdcpVsf4hP/2vX8rZgFqBKhD0/RNIC+B26Lg3qgf0iMqEVAC
YAOWY2GdvR63rk/lnVxtdI75PXXADUfQleyCJRKYrmb/Qs7diUvI0gMN8zBgt7riuSSuZ29OKn0HjzV
bfh8dIJBDSHWthkhaPG1P/4if3E38Pk9qvXEVCaudNaCpGE/NTP2Ac+7mpvl9BkBAeGxgIwrdtlso5c
f4qPsin2Znlh7fiPx02c0XcmP8lrtD6y249xGYKJT90qXs0hs9tWuVw/ym99UcnZVeUXpJ6e8o/T4x5
8cpKfDszfn1NuH/X8d/AxvQTaxLJ9wMAAAAAElFTkSuQmCC
'''

icopg_left = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAN4SURBVHja7Z
fPaxxlGMe/zzvv7GxmN2Zpkk3SLE3bQAkhDRtFDwr20NAFNbaCzaI3T+YPEKoopPVSoV7Vo1Q8eTHm4
EFRPPYoIuihVEExbtHsZnd2M7/e93F238iIhwayuymIzzPfmYGBeT7zfIeZ5yVmxsMMkei/CUDPkKRX
yXooAPQcVSDx00Z542daJ+tYAWiNrrkj7tbmK5ulTtApIcIjeEDIgRUuUwan8NHs5Oz6RnWDiieLuPH
hDcAChg5AV2kuKb61PL9crl6uQmYlvI4HE0MGoOepAuD26hOrUxeevoCQQ/j7PmxhAzRUAOO3Le3rV1
avZOfPzaPpN6FZQ7GCIxwg2RANHCD1u5AvrK9V1qgwXkAraP1dvHcMrXAoAMbv09gqzZTKF5+8CDki4
QUeFCnobnJvjyxnDYA3QAB6gSqwcXvx3OJUeamMkEL4oQ+VpCadQiTJxAYAAwKgKr0uc/bmytlytjRb
QjtoQ1kKWuiuDIAwIJo1SBIoI8DQfQEYvxfo41w+/+Kjcyvk5l14vgclFbQ8KGppI6RdsMjqQXA/HaC
X6TSWaHtydOL8wuQCutHyW9C2htY9GRCTxgphICTL/izofsOL01PfTMiJuZx20dnvgDMMpZUpzArdEF
IAFnodYMm9c2EJCEeAkiOgjt6BS8uXRmo7NXz95VfQWQ3KEaxRC1beArkE7WjETgzlKCAL88QZc1cZS
ZAUR+8Af8Kq+FrxqT+83c/c8+5ivBMioACaNSIdARoAJZIHRR2kEDZANgEW9TcP3H/3/l2+o1ba2tum
ecn58VHAgpE8kN0F+BfECEAuGQv6nQf4Ww75A33Zb++/7Z8IQ0wTIGCUwqQgjgGAa64NbCDh99X1uBl
fBYlduAK2tI0FKcw/7TAdIBocgIGIt9FUj8MSP+ZzY8jZufQiIQWSADlHs+BwiPf4Hr6Py/Vm/XNim0
/Y44BGKmVEipDkcIZS3uaAb8bPeo3mzfaeH52UJVBE5s8XJvIBavdvweEg76g3g7pf3anVGrPqFJzQA
fYBtAHyugBi+GM534o/5Xr02K+13+4WmhMYC8YAD6DWAC04HILv4U60VGvUvqDdDE9HMxDHCJC+F29E
lcZe/VbjTy86Y53FeNY9/pURvxVd81udl777/YfaTMH5BTb28IAY6uKUkuBDCvy/Ov4LbxV7Dx5uBrE
AAAAASUVORK5CYII=
'''

icopg_right = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA
DdgAAA3YBfdWCzAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANrSURBVHja7Z
ffaxRXFMe/585kd93dtPnhYiTBBFrRShqiaKMUhBZhrdqkCmtiX4pSUPwH2lAQn1TIq8R/oQilVR/60
Jc+tRby4EtLoaVUrCmBJrvO3NnMzL13bi93FkaEgnR3kj70LB/u7r58P3POgd1LWmtsZzHDf1+ALpND
p8jdFgE6Tw7+xKOTb51cpTNU3/oOCLwCYPLC6Qu18o7yV/Q+fbItOxCXYyx+vFgarY3epFn6nKapsKU
CHvdQGCjg6kdXaer1qXnswQ/UoPGtESAgCAPwTY6YYjTONnBi5sQ0IiMxS/X8BYpAEAXwQx885PBCD4
dnDqPxXmNXn9tn9yJ3AT824RGHH2XnyPgIFj5YKA30D2R7kZdAW7bBJQcX3MoYbDcK1QLmzszR2NjYP
CbSvchFYFNtwhcmWFpSEWFPuxfH3z6OA/sPTIOMxFmq91SACgyBDsA1B1cdZIqRsAQiwL69+3Bk8sgu
t2r2Yp4+7Z2AS2hTG37iW3jC0/cq64YdScwxNDiEY28cLVX6qzdogd21e9GLEXBtQsiEkDnhp91Ism5
YiSjdDTDg0PhBqg3vbGCSVuhDmsA/lPtSHXAYWMlADEgAnWgopaCRnpGKAAE40gGLGRzhgARhojqBne
XamxvRxrd0nl7Td7X6dwIuYcVfgVTSCkAaYkMEOJEDN3bBIgbdNkKBguJGjmuwkOHozDG8M/1uZfne7
W46QEDFoJAisuGpxARKBWhYbBFQrBbh7ingu7Xvf3oc/T5nn76bEaCs03BpcJBWguw7t4MDVAf7IUuJ
Dnz+AA9148mjJ3FXO2ADK50gYSAbnn12OjDDMCGM41h64qa+k1xHVl2MgAgoUxoY2vAsmKWY3wQIRwG
CNqQnL+pldR8vVJcjSOzSQXZCyWKr4lZQYDvQ1K2f4cnTeln/BlO9EwABwqCQkaQMsWHEgdLNVvNr/K
jO6fs6ArLq3Qi4QSDtQgyQIOxWo1hfa4rI21zSt9Rn6FQOAgzwrYClKIqoBSN4urra0s3kkl5SX8JUf
gKgVCAGXjWvUrOKP9ZXf0VT1PVSOu/8BTzCCO1G2JJ6ja99g4dyNpt33n/JEsJUaT9af3HRetZc0oui
/nx4vh3owzOu/V/W208HQ799RV+TX6CH9VKXUzKlc7rF/n87/hsFAMu0HcYh/QAAAABJRU5ErkJggg=
=
'''

icor_left = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFiElEQVR42r2XA5BcWRiF17YRd084trl
j21MZs9NjhD22bcYZBD12M+mObSfrLawxZ9+NjR68qq/q1r3nP/95fu8FABIRabFHPtpMvC/KTNQuqQ
fhuQucnbe8HG0uWpXkJP67b8sJUCEwYwHijPdLUQ0F+ZEiHBBewpnj34FpIpz+AJTqxUhTUXispfC3L
VUHcPzA1TuEGY4RToUbcYcYRvzkSFOB4pQGiLLifx1pzO9L8edhvOckDu6+eB/8odPY0bYfDbk85ESN
IsF5DBGGnOORxgKtSQeIMhXZMk35v9Rn7oZo/CxEnHPPxMYKMaJtxv5jGPHyWPojb0gcgDqs51uLReA
PnnpuRtnHURDLB8OYe5zaEVWJAgQbDNsEGrB/KVkzgpFdxyjT52dL9T5EmnH+IiEkugY81dpnOSs1DI
fYNKO9cQ/6Ow/fh7F8PPzt8rGOuQHNJSMPrRNqMvhYYcw/Tk6HxHeBmXR2nIl02h9ZCV1gb9p/B7mvf
aA0OzhEfpZvq+zXnj+Guhaio2XPfRpCRigHTGNh3qSeA/q0ldLq85iHPM2ysLGGh50b9mHx5464vb5s
lv9H0l+6srWW+E5UZPags0V8h/ZGEZKcBP9Fmou0JvUk1H+B9YrslwGFqvN9/81K2gLax5Z4UCPzlVu
4+mJvtJZzsLlWeIfKZC55cB2fTIA7LP7CXY/+se0x2sdWgketL/ncvs/XLgtt5TwK7g1aSqjTYD6KSF
Ox4qQDPDXg1z4fS3/l+lPBmp1oKhpDQ8FNkrz6EWMqTp72AAT5Wcvbon3qUJ8/iprsYVRlDiGd0U2dB
sHwpAO4qdfqu6rXnSKQ8aM0SrMDQj1MslCbM4KqjCGUpfQjO64bYYbDpycdgDQ+ILwMAhk/SqNBC6Mb
ycajJmsYFakDYIW3I8iqHoF63b89cwBqe3EyAQylE1G8thdJfpuxwqUFwTaN8NXpfLYA5uYlr1t/xXp
L0lOgsYDh56CVhbUh25DouwnRHm0ItW2Ct/bmc08NUMLofp3h0DZLn5ZJl+w0kQCxVUHW1UiJ6MTqwK
2I99l4I4CneqvgiQEYVPNsRvusNYEdet8szgpUunMUnh2lBQnva9MSrpHGGZHbsS60/cZp8DWug7tGY
9FjA5DmLKp5TtwOvYLVPVGBlo1dBlIp9c8bQHNBXKuvRSUKV/UgO3bnraOwBU4apXBTbdR5ZADnZazX
GA61szIYO/TyEtlRJay+rrLkAdgpF47oSK11fNbmWrQ4V3P59Iky1gBK1vUhL4GNdOZ2RDg3wkYh/+J
jH8XL9RvfsFZI0UuO6IgqWNnTVZ4ygLqckdGCpD7YKhdO6NFZ9Wp01ntPOuxa8xObLRQyJvITu6naUR
APyos6Au2wlE+bcFAqsXziu8BcJt3czSCnq2h1LyrTBkcb8sawvoyPDRV8MF1boS+VfFmHvq5Qe16iu
+b8VXMJWnPjPbXnJxXr0lddiXBpwoZy/o0aqpZ4gHhRnrCUyWh76utYfx7rDYMl8VdjfBpHydOrqWAc
GysE6GgQg73xAFqKuUiP3I5wJ+pwquTeINSpAWmRnWgu5tzQEC2pIbXEI8anAcTTRDb37acGIFgqpBk
bLIn8N9G/CU2FHGyqFKCzSYyeTQcx3HUMvL7TEI+dx2HhFcKNMZkja0RDtKSG1BIP4mWukG7+XJ9kpt
LJlhr00Ouuxiw05I88dwBSQ2qJB/GS6KNUfxnrHd2FzHatxQETabEbbxzewc6j4HSfxO6hs9g7foFwY
0zmyBrREC2pIbXEY9J/RkaLYpyU5nl/byAfhBU+BajOY6OjmY/RnccIN8ZkjqwRDdGSmin/NzRYEmeh
QQvZoDjP65TsLOcfl31t9zeBjMkcWSOaSfyazTz/A4R29YWpIMzKAAAAAElFTkSuQmCC
'''

icor_right = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFiUlEQVR42r2XA3RcaRzF17ZRz6SMbW5
sO6cxpxOjnNi2WTcoJsZEb9JMWNtY78EauTtf7XZm2r5z7jkf/vd+v+f3XgAgtMINJ1rDjSZGQk3GpY
XNEA3AaAJdO48hzm7i7wjDybX29jtefq4ATIMxnDr6HWbGLiA7lAc+EDdKf1rsmQGEGnJlGXpUfLDeS
F+Q7uAJvnB05vIt7SibQaTp2G+hhrxgfvKLTw0gVJ+rFqI7fDTGfhAZYRzUZI5iT9M0qL6TmN1//i4N
dRxHgvcoQvWprjAz6muRAFjaA28w9Eazwi0G/9taMgHe8Jkn09BpVKeNg2lI/RJqNG4jFECYIU+RoT9
yNCeSAod9FFTvCYHVmM8D/3SdFRiALB5qNPzXjvIp/uJHBNbAviMo2MCBrw77F3+dflOBAMhhX6NPHa
1IodDdevA+1RcMYBNzC7ytsqEvHX3ffHPtOAIs6mEvV9PvqtQ8T+BrgKk/lpUSOAz2tum71NIwjkDHX
Eh+7fqj9DzPRrn5/gFSX3vcVZMW0wYD8aQ/jMTTo4S6C0KNeWpxdtz/mmt5aG2YuKWS1A6orfCcE//S
kb1qnvdHN+uXf26LvVumsLViFK5GaVBexDygTVsrLuxzgDxYjpbGj2B75dgtNRYPQ3m5OyS+cgq+10z
72BRpa3dAcbHnv1Jfeudov8B6RegnYajhhCzTmIOGgmE0FY/c0Cg8rdKw4nPrrgeZaR+ZUfSPzM8s/8
JZS+R3QYThRHycWzdqcgavqS5vEDkb9kL8K8efln/t8TGpeRa64/Bz+5MZ7ShL7UNFej+qszkI96iC9
LzVTWT+mQME6fafTI9qR1FCN8pS+lCZMQAXgzTIzfcJfJDRSblS21G56gQRaYsM4KvV/pufWTVYwc0o
SexBRVo/9CSjoUILoj/ISBaeGbsIItIWGcBTo/U3f4tarHFoQJzXduRv7ISueKxIAPztxScGcFfffib
Qsg7hLk2I9dyGjQG7YKOWBpUlDC9hT4H5V6y3jI0LXn8iAFflRi4BiPbYivW+O5EQ0go/83I+QGSZsI
dXm5ZKZ9g0zStgtL/+WABnldo8T/2qa4d/U2AzUkJ3XwNRp8VckVsS876gi8vx9/6b5Wm+G3xbtNIZz
fMYD4G4fUgVazXsVAr5i+64tvfpkXuRu64DnialUF0S1SgogI5YQrWvaW1bzvqOsIyoPVqsh0Dc1bGQ
yT4fYl+LZOZuZMWwUbCpC0WsHhhLJ8+p0aIcn3RxDbGNtlbyuQNF8T0oYHW1ZcWyw1IYe7QYNpXz7Fe
xXnsogI1cgampdNJcQkgzctZ2oDihB1UZHGTHtsNEJmVObXFs/aNOhxKd9Z4WnVVtKZ87lxPXxfcOcE
gGP6stPqQlzFwmQWu1du0bj3wdm0qkNDnpZCBvfSdKk3pRkzWIzUUUthRTCHGogyZ93SX1xXH5agujX
VUXr1tIpL4o1lmDvilXWyz+ItOxEVtKKOIhXpLBIVn8zDZjiWTjx34PGEhmvq2zIvpyhEfNtSdiXc4Q
tpZw0VIzAfbWGdTnDyMptBWBdjWwUMi8pmC7WiSH7kZD/si1GlJLPMRLMiI8ajkkU3sR640n+iQzlkk
21lkR+m+sdx3qcoexrZSL1roJdGybRX/bEYx2ncTE4FkcHLt0TaRNxsgcqSG1xEO8JINkmcok6Qv0UW
ooHm+qQg+86qjPQk32gMAAxEO8JINkCfVZrr2K9Y7mUmaz2nKfuaTIrdcOb2/rYQy3H8f+vtOYHDp3T
aRNxsgcqSG1xEO8JEPkHxO9ZRF2covcv9eR9sMajxyUZ7HRUk+Bs/fINZE2GSNzpIbUEs9T/zXTWRFl
okIL2CK7yO2E5Dz7H1d9bfU3EWmTMTJHakT4N3z++h+JqPWERw4ITwAAAABJRU5ErkJggg==
'''

icowin_close = '''
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
'''

icozoom_in = '''
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGF0lEQVR42sXXA3Qrax4A8G9tm8+2bVu
13b7Uuqxt2+c23XfrbnutIrWNpG3sTKbxTPTfycFaxWZ/54zxWWg/BEIB+lcWlxbQwewjYLVa/a4Sx/
q0apVIo9oDpVIOMplYwOdxGnbZ7GcQZXJyAh05hVz+sEwq5s0tLsGpzHJlZlEDnl3ShBdXt+GpeTV4X
FI+Pj4xBvPzs0tTMzM/RUdJKBK58nhsOJlZK639wxWRmgB8Tw9KTGvGMK0FUxlAietAmV/XK4xOzFcw
Rm7A4PDgU+gosDnsVzc21yEypZ7HkRFirpwQ8DADl4+RbCFG7PKpTYARbL6C5PIUhGCNjYsC4wsEA/2
90N3TfWtzSxM6kJmZabTG3PzqxvoKnMyj87aEBGdDoNtiifWruzJikYuZ5nUAU3pq42PGObacXNyWGF
Y3hbqtZY6KE51ULGmlt7DRYczPz2XX03tVfaOcjZlt9eIyTze5KSZGdjDymgbg/N1Pfai768kP9EoLn
GdjxqtMKTm8wtdPzO1oFumXVrcaz7SZW1vPuOfkZhw0F8ZJ74S2pZEN1dwYUz00zzOc35SRnbu4ka4B
aPA9WbPlc6JqSwVQz9GYzrAwsn1NRJxf4unGp7Z1SzHp9dzVlaVxdBAjjOGbrl+7DK9G9l2gj8r7GSx
t77yIaMIBCkiAdAA4EZnfOxSe1zsCAMetAMk6gGyOytTFxYxTawLDvPvpjvGpSQagg7h0+eIbLW2dZo
eMsZ5YOqe4fFiZIAEovOn+F4RPv+O98djrLmvJ5d365Ipu/WOvuaxR99Z/f+/zfBKgkSUney6vqHp9c
xkXGhrqICDQ9zdov3r7ej6ramzTu+fPdZzuFOUVMHAaF4B202Pvzb0dXtfySlBlXXbTNbFteyWwovat
0Nqmmx59b0YFEHiFoz/eMq6s9C2a7Y47FgMRkWG/R/v1xdm2p2vqm8A1d7YjoUecUzq5F7httGQaAVI
NAEkAEE/L7hgKpTYAiCUATpkAErhWa0LjltaxbBDLDipb6AkLC4KgEP/vof3Kzs/6Zkc7HZzSR/sz+i
XFlTOq4B6MfGVAa3r4vM58pxzg56/4l1S9TG0WgB+e15t/Z7t/QW96rIujd20Y2yuOLLzA8PNz06CDG
mUMzZTRb7AG11TXvlhWn+rHyLduGMyPjxgs91gAfvmoQ2bJI59mlADAD8YJy+9t9wcJ89OX+IbPa24o
Gmkn8rmOzp+2oINgru2i2vraj9bW16Hhquh6+ZAyvZ2v/4SKwJO2gPq0xh/hAIu2LRFXfXWStP6aYTD
fO0eY3xtkG/Iy2nf6vLxdwcXV8TeOzp+hA6O3tUwn5DXwo+iS8opJFe2y1vTKiMH80ARhuakWrF9DlB
ULfHfZaLmFZbK8u6k2JdczNM0xJxK0nzl8XI0OIzHpNLrv3ru+GhsbhgXGZbPj2uWlTQta2rje8vaS0
fIk0wJ3SwFulgE8IQfw2NVb01smNE0JGXm4g9PHK4hiS/2hhEeEouDPA77r7++16h0YQoTmDVwsGbWm
XeBB6KIOfDdJCFxQQ0zfujm/unt6JDQi1PLRJ++P/SXwI+Dr741sXD1cwpycPta5ebpDYHQSPzK5dC0
+u3o18mSalBYaDB98+Ba3s6sDamqrcURx8XD6nqPjJ+jIObs4POTg9EkcVcY51FZMnQdRqb0bUerq61
qYLCbe3NwIQcEBZ9H/ChXgPz1vb29fB8rg0KA1MzsdPnH86D4XNydkN/Fx8VU7OzsgkUqgs7vTGhoeu
oHsxdfXF9kUlxabzWaTdXpmGsorysDTy90D2VMoLTRgfHwc9lR7MHCu3xp/PI5AFB8fH2Q3Kakpu1QE
rKtrK0DNC8HP36cQ2UtISAjy9PR8tKu7C7RaLVy7fhWSUxLh3Q/e/oWD46fIbo4fP97L4XKsrG0WtHe
ctYbQggeRvXh4eKBnn332e9U11aDWqoExyoCCwnxwdnV4NcQvANlNVFRU4vzCPPD4XOsf+/sgOiaSje
wtKztrTyaXEVPTE2RJaTEge+rq6kJBwUFPbmxuALV6hta2M63o/wHA9M3xifGbEaWwtBD9tT8B2EISQ
jWL6JoAAAAASUVORK5CYII=
'''

icozoom_out = '''
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
nwAAAAASUVORK5CYII=
'''

icozoom_view = '''
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
pTOqn8X8C7S8NXA11u60AAAAASUVORK5CYII=
'''

setico = {"info" : [icoinfo, "Informazioni sul documento corrente"],
         "pg_first" : [icopg_first, "Sposta alla prima pagina"],
         "pg_last" : [icopg_last, "Sposta all'ultima pagina"],
         "pg_left" : [icopg_left, "Sposta alla pagina precedente"],
         "pg_right" : [icopg_right, "Sposta alla pagina successiva"],
         "r_left" : [icor_left, "Ruota a sinistra (90°)"],
         "r_right" : [icor_right, "Ruota a destra (90°)"],
         "win_close" : [icowin_close, "Chiudi la finestra"],
         "zoom_in" : [icozoom_in, "Aumenta dimensione immagine"],
         "zoom_out" : [icozoom_out, "Riduce dimensione immagine"],
         "zoom_view" : [icozoom_view, "Adatta l'immagine alla finestra"]
         }


class IcoDispencer:
   def __init__(self):
       self.icoset = setico

   def getIco(self, chiave):
       return self.icoset[chiave][0]

   def getDescr(self, chiave):
       try:
           descr = self.icoset[chiave][1]
           return descr
       except:
           return None

   def printData(self):
       for chiave in self.icoset:
           print(chiave, ':', self.icoset[chiave][1])

   def getData(self):
       return self.icoset

Se si volesse seguire l'esempio, deve risiedere nella stessa direttrice del programma principale

Sui tooltip


I tooltip, ossia quella stringa descrittiva giallina nella figura del bottone esposta in precedenza, sono degli aiuti in linea presenti in molti framework grafici ma non in tkinter (wx li ha). Personalmente, ho una antica abitudine ad utilizzarli, rendono più compatta l'interfaccia utente e permettono di proporre suggerimenti contestuali, tipo formato dei dati da inserire, etc ... li trovo comodi, insomma.
Appena resomi conto della loro assenza in tkinter cercai, e trovai, in quella miniera che è stackoverflow, il modo di implementarli con tale framework, inizialmente sui soli widget ordinari (bottoni, entry, etc.), poi anche sugli item dei menu che NON sono wigdet ordinari, funzionano diversamente.
Inoltre, avevo iniziato a sperimentare un tentativo di unificare le distinte procedure (tra widget e menu), esperimento che abbandonai per approfondire il linguaggio python, date le numerose insufficienze che tutt'ora ho.

I tooltip sono, in effetti, delle finestre toplevel ripulite di tutti i fronzoli non necessari e posizionate sulle coordinate del controllo da esplicitare, non entro in merito perché qui non interessa ma nel codice che segue ("my_tk_object.py") troverete tutte le classi implementate in proposito con i riferimenti di origine nel caso vogliate approfondire. anche my_tk_object deve essere nella direttrice della applicazione del programma principale.

Codice di my_tk_object.py:
CODICE
# -*- coding: utf-8 -*-

import tkinter as tk

# *** CLASSI DI UTILITÀ DERIVATE DA ALTRE TROVATE IN GIRO ED ADATTATE ***

class CreaToolTip(object):
   '''
Crea un tooltip per un generico widget.

Esempio originale tratto da stackoverflow, indirizzo:
https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter

eseguite minime personalizzazioni.
'''
   def __init__(self, controllo, msg=''):
       self.attesa = 500                # Tempo di attesa in millisecondi
       self.lunghezza = 300             # dimensione del messaggio in pixel
       self.controllo = controllo
       self.testo = msg
       # Utilizza gli eventi del controllo chiamante
       self.controllo.bind('<Enter>', self.avvia)
       self.controllo.bind('<Leave>', self.chiudi)
       self.controllo.bind('<ButtonPress>', self.chiudi)
       self.id = None
       self.tw = None

   def avvia(self, event=None):
       self.schedule()

   def chiudi(self, event=None):
       self.unschedule()
       self.nasconditip()

   def schedule(self):
       self.unschedule()
       self.id = self.controllo.after(self.attesa, self.mostratip)

   def unschedule(self):
       iden = self.id
       self.id = None
       if iden:
           self.controllo.after_cancel(iden)

   def mostratip(self, event=None):
       x = y = 0
       x, y, cx, cy = self.controllo.bbox('insert')
       x += self.controllo.winfo_rootx() + 25
       y += self.controllo.winfo_rooty() + 20
       # crea una finestra toplevel con padre il controllo
       self.tw = tk.Toplevel(self.controllo)
       # lascia solo la label e rimuove gli elementi della finestra
       self.tw.wm_overrideredirect(True)
       self.tw.wm_geometry('+%d+%d' % (x, y))
       messaggio = tk.Label(self.tw,
                            text=self.testo,
                            justify='left',
                            background='#f6f6e3',
                            relief='solid',
                            borderwidth=1,
                            wraplength=self.lunghezza
                            )
       messaggio.pack(ipadx=1)

   def nasconditip(self):
       tw = self.tw
       self.tw = None
       if tw:
           tw.destroy()



class MenuTooltip(tk.Menu):
   '''
   da https://stackoverflow.com/questions/55316791/how-can-i-add-a-tooltip-to-menu-item
   permette di aggiungere un tooltip ai menu-items.
   Introdotte variazioni funzionali rispetto all'esempio citato.
   '''
   def __init__(self, parent):
       """
       :parametro parent: il master del Menu, può essere 'root' o 'Menubar'
        .tooltip == Lista di tuple (yposition, text)
        .tooltip_active == Indice (0-based) del Tooltip attivo
        eventi intercettati <Leave>, <Motion>
       """
       super().__init__(parent, tearoff=0)
       #self.menu_ttp
       self.tooltip = []
       self.tooltip_active = None
       self.tooltip_win = None

       self.bind('<Leave>', self.leave)
       self.bind('<Motion>', self.on_motion)

   def add_command(self, *cnf, **kwargs):
       tooltip = kwargs.get('tooltip')
       if tooltip:
           del kwargs['tooltip']
       super().add_command(*cnf, **kwargs)
       # chiede l'indice dell'ultimo command
       u_command = self.index("end")
       self.add_tooltip(u_command, tooltip)

   def add_cascade(self, *cnf, **kwargs):
       tooltip = kwargs.get('tooltip')
       if tooltip:
           del kwargs['tooltip']
       super().add_cascade(*cnf, **kwargs)
       # chiede l'indice dell'ultimo item
       u_item = self.index("end")
       self.add_tooltip(u_item, tooltip)
       
   def add_checkbutton(self, *cnf, **kwargs):
       tooltip = kwargs.get('tooltip')
       if tooltip:
           del kwargs['tooltip']
       super().add_checkbutton(*cnf, **kwargs)
       # chiede l'indice dell'ultimo item
       u_item = self.index("end")
       self.add_tooltip(u_item, tooltip)

   def add_tooltip(self, index, tooltip):
       """
       :parametro index  : Indice (0-based) degli item del Menu
       :parametro tooltip: Testo da mostrare quale Tooltip
       :return: None
       """
       self.tooltip.append((self.yposition(index) + 2, tooltip))

   def on_motion(self, event):
       """
       Cicla i .tooltip per trovare il Menu Item
       """
       for idx in range(len(self.tooltip) - 1, -1, -1):
           if event.y >= self.tooltip[idx][0]:
               x = event.x_root
               y = event.y_root
               point = (x, y)
               self.show_tooltip(idx, point)
               break

   def leave(self, event):
       """
       Distrugge il Tooltip corrente e resetta .tooltip_active a None
       """
       if not self.tooltip_active is None:
           # destroy(<tooltip_active>)
           if self.tooltip_win:
               self.tooltip_win.chiudi()
           self.tooltip_active = None

   def show_tooltip(self, idx, point):
       """
       Mostra il Tooltip se non presente, distrugge il Tooltip attivo
       :parametro idx: Indice del Tooltip mostrato
       :point        : coordinate di posizionamento del tooltip
       :return: None
       """
       if self.tooltip_active != idx:
           # destroy(<tooltip_active>)
           if self.tooltip_win:
               self.tooltip_win.chiudi()
           # create new tooltip
           self.tooltip_active = idx
           msg = self.tooltip[idx][1]
           self.tooltip_win = ToolTipWin(self, point, msg)
           self.tooltip_win.avvia()


# *** MIE CLASSI ***

class ToolTipWin(object):
   '''
Ricodifica della classe CreaToolTip mirata alla esposizione di tooltip
in un menu tkinter (gli oggetti items non vengono restituiti quali widget)
'''
   def __init__(self, ctrl, point, msg=''):
       self.attesa = 500
       self.lunghezza = 300
       self.ctrl = ctrl
       self.text = msg
       self.point = point
       self.id = None
       self.tw = None

   def avvia(self):
       self.unschedule()
       self.id = self.ctrl.after(self.attesa, self.mostra_tip)

   def chiudi(self):
       self.unschedule()
       self.nascondi_tip()

   def unschedule(self):
       iden = self.id
       self.id = None
       if iden:
           self.ctrl.after_cancel(iden)

   def mostra_tip(self):
       x, y = self.point
       # crea una finestra toplevel con padre il menu
       self.tw = tk.Toplevel(self.ctrl)
       # lascia solo la label e rimuove gli elementi della finestra
       self.tw.wm_overrideredirect(True)
       self.tw.wm_geometry('+%d+%d' % (x, y))
       messaggio = tk.Label(self.tw,
                           text=self.text,
                           justify='left',
                           background='#f6f6e3',
                           relief='solid',
                           borderwidth=1,
                           wraplength=self.lunghezza
                           )
       messaggio.pack(ipadx=1)

   def nascondi_tip(self):
       tw = self.tw
       self.tw = None
       if tw:
           tw.destroy()

la classe "CreaToolTip" viene utilizzata nell'esempio corrente, quelle inerenti i menu verranno, probabilmente, utilizzate più in la.

Come si applicano icone e tooltip


Naturalmente, le modalità di rappresentazione da me adottate incidono un pochino sul naturale svolgimento della codifica, anche se prevalgono comunque i metodi di tkinter.
Una prima circostanza da tener ben presente è che in tkinter una immagine assegnata ad un controllo NON viene memorizzata, viene persa non appena si esce dai metodi di inizializzazione del controllo, è necessario definirla quale variabile di istanza della classe di appartenenza perché possa essere utilizzata.
Avendo acquisito la "abitudine" all'uso delle variabili di istanza, che trovo comode anche in relazione al controllo del contesto d'uso dei widget, lo utilizzo anche in caso non sia stettamente necessario, comunque, prendendo ad esempio il pulsante di navigazione in figura, definito dai seguenti frammenti di codice :
CODICE
import viewer_ico_and_tip as IaT
from my_tk_object import CreaToolTip as ctt
       ...
       ids = IaT.IcoDispencer()
       ...
       self.ico_pglast = tk.PhotoImage(data=ids.getIco('pg_last'))
       self.bt_last = tk.Button(p_tools, image=self.ico_pglast, name='bt_last')
       self.bt_last_ttp = ctt(self.bt_last, ids.getDescr('pg_last'))

ove utilizzo, importando, i due moduli prima esposti istanziando un oggetto "IcoDispencer" che provvede a fornire l'immagine (in codifica b64) ed il testo correlati alla chiave utilizzata, con l'immagine codificata costruisco un oggetto PhotoImage di tkinter assegnandolo alla variabile di istanza "ico_pglast" che, a sua volta, viene assegnata quale parametro "image" nello istanziamento di un "Button" (pulsante) tkinter, assegnato alla variabile di istanza "bt_last", a sua volta assegnato quale paramentro, assieme al testo ottenuto da IcoDispencer, nella istanza di un oggetto "CreaToolTip", assegnata alla variabile di istanza "bt_last_ttp" e della quale (il pulsante) diviene riferimento.

Insomma, un bel gioco di scatole cinesi, come tutta la programmazione ad oggetti.
Le particolarità preliminari sono concluse, nel prossimo post tratteremo effettivamente l'operatività del pdf viewer versione tkinter.
 
Web  Top
view post Posted on 9/5/2021, 06:21
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


Ora che sono state viste le "particolarità" derivate dai miei vezzi, passiamo all'effettivo esame del prototipo di visualizzatore di PDF

L'interfaccia utente


La finestra del visualizzatore, di per se, è molto semplice, si tratta essenzialmente di un canvas, un paio di barre di scorrimento, un insieme di pulsanti, ed una entry, per fare alcune elementari operazioni ed un paio di frame a far da pannelli contenitore.
Riguardo ai controlli dedicati alle operazioni di comando, possono essere così schematizzati :

png

  • Controlli di navigazione - una entry per indicare direttamente il numero di pagina che si vuole visualizzare e quattro pulsante per spostarsi rispettivamente, andando dalla vostra sinistra a destra, alla prima pagina, alla pagina precedente, alla pagina successiva, all'ultima pagina, quando si inserisce un numero nella entry bisogna premere "Invio" perché lo spostamento venga eseguito;
  • Comandi di rotazione - per ruotare di 90° la pagina verso sinistra o verso destra, gli angoli considerati al momento sono 0, 90, 180 e 270, la rotazione è applicata a tutte le pagine del documento;
  • Comandi di zoom - Incrementano/decrementano del 10% le dimensioni delle pagine visualizzate ovvero adattano la pagina all'area disponibile (condizione di default) il range di impostazione va dal 10% al 400%, sempre da sinistra a destra : 1° decremento, 2° incremento, 3° switch Adatta/Dimensioni reali;
  • Pulsante informazioni - fa comparire una finestra di dialogo con le informazioni generali lette dal documento
  • Pulsante chiusura finestra - ... basta la parola ;) .
I comandi relativi alla navigazione, rotazione e zoom hanno immediato riflesso sull'immagine esposta che viene ridefinita e mostrata nel canvas dedicato alla visualizzazione.

Come funzionano le cose


Ribadisco che ora si sta trattando un prototipo preliminare di fattibilità per la sola visualizzazione, al momento molto viene ignorato e tutti i processi avvengono "dentro" la finestra stessa, nel senso che sono implementati all'interno della classe PdfViewer, sub-classamento di un oggetto TopLevel di tkinter, ciò al momento, per un utilizzo "operativo" le cose cambieranno.
PdfViewer espone il metodo "Set_File(f_pathname)" che permette ad un processo esterno di impostare il path-name del file da visualizzare, in tale metodo viene verificata l'effettiva esistenza del file indicato che, se trovato, viene memorizzato quale documento corrente per l'istanza utilizzata, niente vieta che PdfViewer possa avere istanze concorrenti ma, al momento, la cosa darebbe problemi perché viene ripulita la directory dedicata alla cache delle immagini di pagina dei documenti, prima di dare il documento ricevuto in pasto pdf2image per l'estrazione delle informazioni sul documento, tramite la funzione pdf2info.pdfinfo_from_path(path_name), che vengono memorizzate. Immediatamente dopo ripulita una eventual cache ("PdfViewer._clear_cache()") presente e chiamato il metodo della classe "_pdf_cache()"
CODICE
def _pdf_cache(self):
       self.pdf_images = []
       for p in range(0, self.pages+1, 10):
           images = p2i.convert_from_path(self.doc,
                                          first_page=p, last_page=min(p+10-1, self.pages),
                                          fmt='jpeg', output_folder='my_tmp/pdfcache')
           for i in images:
               self.pdf_images.append(i)

È il caso di dare una guardata a questo codice.
In primo luogo possiamo vedere che viene definita una proprietà della istanza corrente di PdfViewer, "self.pdf_images" che è una lista delle immagini generate da pdf2image, Dette immagini sono memorizzare su file, resi temporanei dalla applicazione, memorizzati nella direttrice di cache definita ("<appdir>/my_tmp/pdfcache" al momento).

In merito alla generazione dei files immagine, osservate il ciclo for : vengono generate 10 pagine alla volta ... alla pagina del progetto pdf2image, proprio in fondo, è scritto : Un PDF relativamente grande consumerà tutta la tua memoria e causerà l'arresto del processo (a meno che tu non usi una cartella di output) (scusate la dozzinale traduzione)
Or bene, sono decenni che trasformo pagine PDF in immagini, utilizzando imagemagik per lo più, utilizzando processi analoghi, ho rilevato che quando i files pdf sono "grossi" immancabilmente arriva il crash, la memoria è molto più veloce dell'output su disco, dopo numerose prove ho determinato che operare su una decina di pagine alla volta è un buon compromesso, liberi di fare Vostre scelte ma io mi attengo a tale criterio anche avendo stabilito un output_folder.

Comunque, tralasciando la disgressione, viene utilizzata la funzione "pdf2image.convert_from_path()" per la conversione delle singole pagine in immagini, in vero, si "potrebbe" estrarre una singola pagina mantenendola in memoria, con "pdf2image.convert_from_bytes()", ci ho provato e funziona ma i tempi di attesa sono inaccettabilmente lunghi.

Dal punto di vista funzionale la finestra, al momento, si limita a visualizzare la pagina corrente, riposizionando controlli ed, eventualmente, il contenuto in caso di ridimensionamento della finestra. Tale riposizionamento viene effettuato, nel caso dei controlli, sfruttando la proprietà "weight" di righe e colonne definite tramite il gestore di geometria grid, nel metodo "_populate(self)" della classe PdfViewer :
CODICE
...
       p_tools.grid_columnconfigure(7, weight=1)
       p_tools.grid_columnconfigure(10, weight=1)
       p_tools.grid_columnconfigure(14, weight=1)
       p_tools.grid_columnconfigure(16, weight=1)
...
       p_img.grid_rowconfigure(0, weight=1)
       p_img.grid_columnconfigure(0, weight=1)
       self.grid_columnconfigure(0, weight=1)
       self.grid_rowconfigure(1, weight=1)
...

essa viene applicata specificatamente ai vari pannelli (frame) per le proprie esigenze di gestione.

Da tener presente che l'immagine della pagina corrente, in caso si sia nella condizione di adattamento alla dimensione finestra (self.adapt = True), viene ridisegnata adattandola alle dimensioni del canvas contenitore.

... In merito al "ridisegno" della immagine, c'è da dire che in un primo momento lo avevo implementato il maniera tale che si verificasse al ridimensionamento del canvas che la rappresenta, constatando un ritardo di riposta nella riconfigurazione dei controlli della finestra, principalmente riguardo ai pulsanti di zoom e rotazione, tant'è che ho provato a ritardarne il ridisegno nelle rispettive istruzioni di callback (self.after(200, self._show_page)), senza grossi risultati.
Empiricamente, sono giunto alla conclusione che variando le dimensioni della immagine, cosa che avviene tanto zoommando, quanto ruotando, anche le dimensioni del canvas si modificano, scatenando un nuovo evento "<configure>" sul canvas, con conseguente nuovo ridisegno ... sarà giusto?, non ne sono certo ma eliminando detto binder dal canvas e spostandolo sul ridimensionamento della finestra i problemi "di ridisegno" dei controlli sono svaniti. Ho lasciato, commentata nel codice, l'istruzione del binding per il ridimensionamento del canvas, nel caso qualcuno volesse effettuare delle prove.

In merito a rotazioni, zoom ed adattamento, essi sono controllate da una serie di variabili di istanza definite nello "__init__" della classe
CODICE
...
class PdfViewer(tk.Toplevel):
   def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.title('tkPDFViewer => Nessun File')
       self.doc = ''
       self.pages = 0
       self.page = 0
       self.adapt = True
       self.zoom = 0.0
       self.rotation = 0
       self._populate()
...

Ve ne sono varie, come si può vedere, banalmente, "self.pages" e "self.page" sono destinate a contenere il numero complessivo di pagine e la pagina corrente del documento, "self.adapt" controlla l'adattamento dell'immagine alla finestra, "self.zoom" e "self.roration" stabiliscono, rispettivamente, i fattori di zoom e rotazione da applicare alla immagine originale.
le variabili relative alle pagine vengono definite/utilizzate in svariati punti della classe, le variabili di zoom e rotazione utilizzate solo nel metodo "self._show_page(self)".

Vi è, inoltre, un'altra variabile di istanza "importante" di cui abbiamo già parlato : "self.pdf_images"

Navigazione pagine documento


Piuttosto scontato come metodo, avviene tramite l'utilizzo di quattro pulsanti, "self.bt_first", "self.bt_previous", "self.bt_next", "self.bt_last", ed una casella di immissione testo, "self.e_page", la definizione di tali controlli quale variabili di istanza non è strettamente necessaria ma è comoda sotto vari aspetti, tutti tali controlli hanno il metodo "self._set_page(self, evt)" quale callback dei loro binding
CODICE
...
   def _set_page(self, evt):
       if not self.doc or not self.pdf_images: return
       w_name = str(evt.widget).split('.')[-1]
       if w_name == 'bt_first':
           self.page = 0
       elif w_name == 'bt_previous':
           self.page -= 1
           if self.page < 0: self.page = 0
       elif w_name == 'e_page':
           try:
               value = int(self.e_page.get())
               if 0 <= (value - 1) <= self.pages - 1:
                   self.page = value - 1
               else:
                   raise ValueError('Inserimento non valido')
           except ValueError:
               message = 'Numero di pagina non valido'
               msgb.showinfo('Selezione pagina', message)
               return
       elif w_name == 'bt_next':
           self.page += 1
           if self.page > self.pages-1: self.page = self.pages -1
       elif w_name == 'bt_last':
           self.page = self.pages - 1
       self._show_page()
...

Da notare come l'algoritmo applicato si basi sul nome ricavato dal descrittore dell'oggetto scatenante l'evento per decidere la pagina da rendere "corrente" prima di invocarne il ridisegno. Si è provveduto ad assegnare il "nome" ai controlli al momento della loro istanza.
Data l'intrinseca semplicità del codice non ritengo vi sia bisogno di ulteriori spiegazioni.
L'immagine sottostante mostra l'effetto del pulsante di spostamento alla pagina successiva nelle condizioni di default per la visualizzazione

png



Rotazione pagine


Anche questa è una funzione molto semplice, due pulsanti, "self.bt_r_left" e "self.bt_r_right", per rotazione a destra o a sinistra, in realtà ne sarebbe stato sufficiente uno, entrambi riferenti al metodo "self._rotate(self, evt)" per i callback
CODICE
...
   def _rotate(self, evt):
       w_name = str(evt.widget).split('.')[-1]
       if w_name == 'bt_r_left':
           self.rotation += 90
       elif w_name == 'bt_r_right':
           self.rotation -= 90
       if self.rotation > 270:
           self.rotation = 0
       elif self.rotation < 0:
           self.rotation = 270
       self.after(200, self._show_page)
...

Nuovamente, è il nome del widget scatenante l'evento a determinare l'angolo di rotazione da applicare all'immagine, espresso in gradi sessagesimali, ogni singola pressione varia di +/- 90° l'angolo ri rotazione corrente contenendolo nel range di valori ammessi : 0, 90, 180 e 270.
L'angolo di rotazione definito è, per il momento, applicato alla visualizzazionwe tutte le pagine di un documento.
L'immagine sottostante mostra l'effetto di una rotazione a sinistra applicata alla precedente immagine

png



Adattamento e zoom della visualizzazione


Per lo zoom delle pagine, sono dedicati i pulsanti del terzo gruppo in figura all'inizio di questo post, i tre pulsanti, dalla vostra sinistra andando verso destra, consentono, i primi due rispettivamente, di ridurre del 10% la dimensione dell'immagine e di aumentarla del 10%, entro un range che va dal 10% al 400% della dimensione effettiva.
La funzione di callback che entra in azione è :
CODICE
def _set_zoom(self, evt):
       w_name = str(evt.widget).split('.')[-1]
       if w_name == 'bt_zout':
           self.zoom -= 0.1
           if self.zoom < -0.9:
               self.zoom = -0.9
               return
       elif w_name == 'bt_zin':
           self.zoom += 0.1
           if self.zoom > 3.00:
               self.zoom = 3.00
               return
       self.bt_zout.configure(state='disabled')
       self.bt_zin.configure(state='disabled')
       self.after(200, self._show_page)
       self.bt_zout.configure(state='normal')
       self.bt_zin.configure(state='normal')

ancora una volta, nella definizione dell'azione da intraprendere, entra in gioco il nome del widget ... ma notate le ultime 5 righe, decisamente "strane", esse sono "frutto" di tentativi di controllo del ridisegno dei widget nella predisposizione iniziale con binding sul ridimensionamento del canvas, indicato in precedenza, contenente l'immagine : i pulsanti restavano "premuti" per eccessivo ritardo causato dai molteplici cicli di re-paint che si scatenavano.
Ho lasciato li quel codice nel caso ci si voglia provare ma sparirà, l'istruzione "self.after(200, self._show_page)! (che inserisce un ritardo di 200 millisecondi) dovrebbe essere sufficiente.
Comunque, sotto vedete l'effetto dell'immagine ruotata precedente con qualche fattore di zoom applicato

png



Entrambi i pulsanti appena trattati vengono abilitati o disabilitati dall'azione del terzo pulsante, quest'ultimo è una specie di switch, modifica la condizione della variabile di istanza "self.adapt" da False a True e viceversa, quanto detta variabile è vera i due pulsanti di zoom vengono disabilitati, abilitati alrimenti
CODICE
...
   def _adapt(self):
       self.adapt = not self.adapt
       if self.adapt:
           self.zoom = 0.0
           self.bt_zout.configure(state='disabled')
           self.bt_zin.configure(state='disabled')
       else:
           self.bt_zout.configure(state='normal')
           self.bt_zin.configure(state='normal')
       self._show_page()
...

noterete senz'altro che la variabile di istanza "self.zoom" viene azzerata, non ha molto senso parlare di zoom quando l'immagine viene adattata dinamicamente ad una superficie variabile. Per altro passando alla visualizzazione a "dimensione fissa" (fatte salve le zoommate) l'immagine viene visualizzata alla sua dimensione "effettiva" che è quella della dimensione pagina (probabilmente A4) a 200 DPI di risoluzione

png


... piuttosto grande, non trovate ;)

Applicazione delle impostazioni date


Vi sarete senz'altro accorti che quasi tutti i metodi di callback visti includono una chiamata al metodo di classe "self._show_page()", detto metodo è il "cuore" del visualizzatore
CODICE
def _show_page(self):
       self.config(cursor='watch')
       self.update()
       self.cnv_img.delete('all')
       image = self.pdf_images[self.page]
       # eventuale rotazione
       if self.rotation:
           image = image.rotate(self.rotation, expand=True)
       img_x, img_y = image.size
       if self.adapt:
           # calcola i rapporti di scala
           r_img = img_x / img_y
           r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
           if r_cnv <= r_img:
               f_scala = self.cnv_img.winfo_width() / img_x
           else:
               f_scala = self.cnv_img.winfo_height() / img_y
           fx = int(img_x * f_scala)
           fy = int(img_y * f_scala)
           image = image.resize((fx, fy))
           x = self.cnv_img.winfo_width() // 2
           y = self.cnv_img.winfo_height() // 2
           x_region = self.cnv_img.winfo_width()
           y_region = self.cnv_img.winfo_height()
       elif self.zoom != 0.0:
           f_x = int(img_x * (1 + self.zoom))
           f_y = int(img_y * (1 + self.zoom))
           image = image.resize((f_x, f_y))
           x = f_x // 2
           y = f_y // 2
           x_region = f_x
           y_region = f_y
       else:
           x = img_x // 2
           y = img_y // 2
           x_region = img_x
           y_region = img_y
       self.img = ImageTk.PhotoImage(image)
       self.cnv_img.create_image(x, y, image=self.img, anchor='center')
       self.cnv_img.configure(scrollregion=(0, 0, x_region, y_region))
       self.cnv_img.update()
       self.e_page.delete(0, tk.END)
       self.e_page.insert(0, '%d' % (self.page + 1))
       self.e_page.update()
       self.config(cursor='')
       self.update()

malgrado la sua "lunghezza", occupata più che altro dal calcolo delle impostazioni di visualizzazione, è piuttosto semplice limitandosi ad ottenere l'immagine di indice corrente, ruotarla se è il caso ed, eventualmente, ridimensionarla.
Le "immagini" fornite dal pdf2image hanno tutto il necessaire per le operazioni di ridimensionamento e rotazione occorrenti, essenzialmente presentano le caratteristiche funzionali di un oggetto Image di PIL (anche se non completamente), l'accorgimento da tener presente è la conversione ad immagine compatibile con tkinter tramite l'istruzione "self.img = ImageTk.PhotoImage(image)"

Ultima piccolezza


Gli ultimi due pulsanti sono piuttosto banali, quello etichettato "4" nella prima figura del post espone le informazioni sul documento aperto, raccolte da pdf2image, l'ultimo procede a chiudere la finestra, ripulendo la cache e distruggendo la finestra
... badate bene, chiude la finestra NON l'applicazione, la GUI di cui qui si discute è un prototipo mirato ad essere integrato in un contesto più ampio che si raggiungerà (spero) tra molti post, dopo un lungo percorso.

Torniamo un pochino sul "pulsante 4", con un certo raccapriccio, lo confesso, ho utilizzato i dialoghi standard di tkinter, trovandoli visivamente molto sgradevoli nel mio sistema e molto poco "maneggiabili", definita la funzione "self._show_info(self)" associata al command del pulsante
CODICE
...
   def _show_info(self):
       message = ''
       maxsize = 0
       for key in self.info.keys():
           if len(key) > maxsize: maxsize = len(key)
       for key in self.info.keys():
           message += key + ' '*(maxsize-len(key)) + ' : ' + str(self.info[key]) + '\n'
       msgb.showinfo('File aperto con successo', message)
...

ho avuto questa raccapricciante resa visiva nel mio desk (gnome3 su ubuntu 20.04)

png


Noterete, dal codice, che ho cercato di allineare i valori informativi alla massima lunghezza delle chiavi di informazione ... con risultati ben miseri, il massimo che mi è riuscito ad ottenere è questo :

png


ottenuto dando la direttiva
CODICE
...
       # configuro font e lunghezza messaggi per le msgbox
       self.option_add('*Dialog.msg.font', 'Courier 9')
...

nella inizializzazione della finestra, nel codice che seguirà alla fine del post troverete un paio di insoddisfacenti tentativi per allargare la label del messaggio : niente da fare! Quel maledetto Title va sempre a capo allo stesso punto e non c'è verso (beh, non lo ho trovato) di sub-classare la finestra di dialogo e manipolarne l'output (cosa che in wx si può fare)

In futuro, per tkinter, costruirò delle finestre di dialogo personali!

Per finire, il sorgete intero del pdfviewer:


CODICE
# -*- coding: utf-8 -*-

import tkinter as tk
import tkinter.messagebox as msgb
import viewer_ico_and_tip as IaT
from my_tk_object import CreaToolTip as ctt
import pdf2image as p2i
import os
import glob
from PIL import ImageTk, Image
from io import BytesIO

# images = p2i.convert_from_bytes(open('pdf_file', 'rb').read())

class PdfViewer(tk.Toplevel):
   def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.title('tkPDFViewer => Nessun File')
       self.doc = ''
       self.pages = 0
       self.page = 0
       self.adapt = True
       self.zoom = 0.0
       self.rotation = 0
       self._populate()
       # configuro font e lunghezza messaggi per le msgbox
       self.option_add('*Dialog.msg.font', 'Courier 9')
       #self.option_add('*Dialog.msg.width', 100)
       #self.option_add('*Dialog.msg.wraplength', '24i')
   
   def _populate(self):
       # TOOLS-BAR
       p_tools = tk.Frame(self)
       p_tools.grid(row=0, column=0, sticky='ew')
       lbl = tk.Label(p_tools, text='Pagina:', justify='left')
       lbl.grid(row=0, column=0, padx=5, pady=5)
       ids = IaT.IcoDispencer()
       # controlli navigazione pagine
       self.ico_pgfirst = tk.PhotoImage(data=ids.getIco('pg_first'))
       self.bt_first = tk.Button(p_tools, image=self.ico_pgfirst, name='bt_first')
       bt_first_ttp = ctt(self.bt_first, ids.getDescr('pg_first'))
       self.bt_first.grid(row=0, column=1, padx=5, pady=5, sticky='nsew')
       self.ico_pgleft = tk.PhotoImage(data=ids.getIco('pg_left'))
       self.bt_previous = tk.Button(p_tools, image=self.ico_pgleft, name='bt_previous')
       bt_previous_ttp = ctt(self.bt_previous, ids.getDescr('pg_left'))
       self.bt_previous.grid(row=0, column=2, padx=5, pady=5, sticky='nsew')
       self.e_page = tk.Entry(p_tools, width=5, name='e_page')
       dida = 'Inserire la pagina da visualizzare'
       e_page_ttp = ctt(self.e_page, dida)
       self.e_page.grid(row=0, column=3, padx=5, pady=5, sticky='ew')
       self.lbl_pages = tk.Label(p_tools, text='di 100.000')
       self.lbl_pages.grid(row=0, column=4, padx=5, pady=5)
       self.ico_pgright = tk.PhotoImage(data=ids.getIco('pg_right'))
       self.bt_next = tk.Button(p_tools, image=self.ico_pgright, name='bt_next')
       bt_next_ttp = ctt(self.bt_next, ids.getDescr('pg_right'))
       self.bt_next.grid(row=0, column=5, padx=5, pady=5, sticky='nsew')
       self.ico_pglast = tk.PhotoImage(data=ids.getIco('pg_last'))
       self.bt_last = tk.Button(p_tools, image=self.ico_pglast, name='bt_last')
       bt_last_ttp = ctt(self.bt_last, ids.getDescr('pg_last'))
       self.bt_last.grid(row=0, column=6, padx=5, pady=5, sticky='nsew')
       # controlli rotazione pagine
       self.ico_rleft = tk.PhotoImage(data=ids.getIco('r_left'))
       self.bt_r_left = tk.Button(p_tools, image=self.ico_rleft, name='bt_r_left')
       bt_r_left_ttp = ctt(self.bt_r_left, ids.getDescr('r_left'))
       self.bt_r_left.grid(row=0, column=8, padx=5, pady=5, sticky='nsew')
       self.ico_rright = tk.PhotoImage(data=ids.getIco('r_right'))
       self.bt_r_right = tk.Button(p_tools, image=self.ico_rright, name='bt_r_right')
       bt_r_right_ttp = ctt(self.bt_r_right, ids.getDescr('r_right'))
       self.bt_r_right.grid(row=0, column=9, padx=5, pady=5, sticky='nsew')
       # controlli zoom pagine
       self.ico_zout = tk.PhotoImage(data=ids.getIco('zoom_out'))
       self.bt_zout = tk.Button(p_tools, image=self.ico_zout, state='disabled', name='bt_zout')
       bt_zout_tpp = ctt(self.bt_zout, ids.getDescr('zoom_out'))
       self.bt_zout.grid(row=0, column=11, padx=5, pady=5, sticky='nsew')
       self.ico_zin = tk.PhotoImage(data=ids.getIco('zoom_in'))
       self.bt_zin = tk.Button(p_tools, image=self.ico_zin, state='disabled', name='bt_zin')
       bt_zin_tpp = ctt(self.bt_zin, ids.getDescr('zoom_in'))
       self.bt_zin.grid(row=0, column=12, padx=5, pady=5, sticky='nsew')
       self.ico_zview = tk.PhotoImage(data=ids.getIco('zoom_view'))
       self.bt_zview = tk.Button(p_tools, image=self.ico_zview, command=self._adapt)
       bt_zview_tpp = ctt(self.bt_zview, ids.getDescr('zoom_view'))
       self.bt_zview.grid(row=0, column=13, padx=5, pady=5, sticky='nsew')
       self.ico_info = tk.PhotoImage(data=ids.getIco('info'))
       self.bt_info = tk.Button(p_tools, image=self.ico_info, command=self._show_info)
       bt_info_tpp = ctt(self.bt_info, ids.getDescr('info'))
       self.bt_info.grid(row=0, column=15, padx=5, pady=5, sticky='nsew')
       self.ico_close = tk.PhotoImage(data=ids.getIco('win_close'))
       self.bt_close = tk.Button(p_tools, image=self.ico_close, command=self._on_closing)
       bt_close_tpp = ctt(self.bt_close, ids.getDescr('win_close'))
       self.bt_close.grid(row=0, column=17, padx=5, pady=5, sticky='nsew')
       p_tools.grid_columnconfigure(7, weight=1)
       p_tools.grid_columnconfigure(10, weight=1)
       p_tools.grid_columnconfigure(14, weight=1)
       p_tools.grid_columnconfigure(16, weight=1)
       p_img = tk.Frame(self)
       p_img.grid(row=1, column=0, sticky='nsew')
       self.cnv_img = tk.Canvas(p_img)
       self.cnv_img.grid(row=0, column=0, sticky='nsew')
       s_v = tk.Scrollbar(p_img, orient=tk.VERTICAL,
                          command=self.cnv_img.yview)
       s_v.grid(row=0, column=1, sticky='ns')
       s_h = tk.Scrollbar(p_img, orient=tk.HORIZONTAL,
                          command=self.cnv_img.xview)
       s_h.grid(row=1, column=0, sticky='ew')
       self.cnv_img.configure(yscrollcommand=s_v.set, xscrollcommand=s_h.set)
       p_img.grid_rowconfigure(0, weight=1)
       p_img.grid_columnconfigure(0, weight=1)
       self.grid_columnconfigure(0, weight=1)
       self.grid_rowconfigure(1, weight=1)

       #self.cnv_img.bind('<Configure>', self._on_resize)
       self.bt_first.bind('<Button-1>', self._set_page)
       self.bt_first.bind('<Return>', self._set_page)
       self.bt_first.bind('<KP_Enter>', self._set_page)
       self.bt_previous.bind('<Button-1>', self._set_page)
       self.bt_previous.bind('<Return>', self._set_page)
       self.bt_previous.bind('<KP_Enter>', self._set_page)
       self.e_page.bind('<Return>', self._set_page)
       self.e_page.bind('<KP_Enter>', self._set_page)
       self.bt_next.bind('<Button-1>', self._set_page)
       self.bt_next.bind('<Return>', self._set_page)
       self.bt_next.bind('<KP_Enter>', self._set_page)
       self.bt_last.bind('<Button-1>', self._set_page)
       self.bt_last.bind('<Return>', self._set_page)
       self.bt_last.bind('<KP_Enter>', self._set_page)

       self.bt_r_left.bind('<Button-1>', self._rotate)
       self.bt_r_left.bind('<Return>', self._rotate)
       self.bt_r_left.bind('<KP_Enter>', self._rotate)
       self.bt_r_right.bind('<Button-1>', self._rotate)
       self.bt_r_right.bind('<Return>', self._rotate)
       self.bt_r_right.bind('<KP_Enter>', self._rotate)


       self.bt_zout.bind('<Button-1>', self._set_zoom)
       self.bt_zout.bind('<Return>', self._set_zoom)
       self.bt_zout.bind('<KP_Enter>', self._set_zoom)
       self.bt_zin.bind('<Button-1>', self._set_zoom)
       self.bt_zin.bind('<Return>', self._set_zoom)
       self.bt_zin.bind('<KP_Enter>', self._set_zoom)

       self.bind('<Configure>', self._on_resize)
       self.protocol('WM_DELETE_WINDOW', self._on_closing)
       self.update()
   
   # *** CALLBACK BINDING ***
   def _on_resize(self, evt):
       if self.doc and self.pdf_images:
           self._show_page()
       else:
           return
   
   def _on_closing(self):
       self._clear_cache()
       self.destroy()
   
   def _set_page(self, evt):
       if not self.doc or not self.pdf_images: return
       w_name = str(evt.widget).split('.')[-1]
       if w_name == 'bt_first':
           self.page = 0
       elif w_name == 'bt_previous':
           self.page -= 1
           if self.page < 0: self.page = 0
       elif w_name == 'e_page':
           try:
               value = int(self.e_page.get())
               if 0 <= (value - 1) <= self.pages - 1:
                   self.page = value - 1
               else:
                   raise ValueError('Inserimento non valido')
           except ValueError:
               message = 'Numero di pagina non valido'
               msgb.showinfo('Selezione pagina', message)
               return
       elif w_name == 'bt_next':
           self.page += 1
           if self.page > self.pages-1: self.page = self.pages -1
       elif w_name == 'bt_last':
           self.page = self.pages - 1
       self._show_page()
   
   def _adapt(self):
       self.adapt = not self.adapt
       if self.adapt:
           self.zoom = 0.0
           self.bt_zout.configure(state='disabled')
           self.bt_zin.configure(state='disabled')
       else:
           self.bt_zout.configure(state='normal')
           self.bt_zin.configure(state='normal')
       self._show_page()
   
   def _set_zoom(self, evt):
       w_name = str(evt.widget).split('.')[-1]
       if w_name == 'bt_zout':
           self.zoom -= 0.1
           if self.zoom < -0.9:
               self.zoom = -0.9
               return
       elif w_name == 'bt_zin':
           self.zoom += 0.1
           if self.zoom > 3.00:
               self.zoom = 3.00
               return
       self.bt_zout.configure(state='disabled')
       self.bt_zin.configure(state='disabled')
       self.after(200, self._show_page)
       self.bt_zout.configure(state='normal')
       self.bt_zin.configure(state='normal')
   
   def _rotate(self, evt):
       w_name = str(evt.widget).split('.')[-1]
       if w_name == 'bt_r_left':
           self.rotation += 90
       elif w_name == 'bt_r_right':
           self.rotation -= 90
       if self.rotation > 270:
           self.rotation = 0
       elif self.rotation < 0:
           self.rotation = 270
       self.after(200, self._show_page)
   
   # *** METODI ***
   def _clear_cache(self):
       files = glob.glob('my_tmp/pdfcache/*.*')
       for f in files:
           try:
               os.remove(f)
           except OSError:
               pass

   def set_file(self, f_pathname):
       if not os.path.exists(f_pathname) or not os.path.isfile(f_pathname):
           return
       self.doc = f_pathname
       f_name = os.path.basename(f_pathname)
       self.title('tkPDFViewer => ' + f_name)
       self.config(cursor='watch')
       self.update()
       self._clear_cache()
       self.info = p2i.pdfinfo_from_path(self.doc)
       self.pages = self.info['Pages']
       self.lbl_pages.configure(text='di %d'%self.pages)
       self.page = 0
       self.e_page.delete(0, tk.END)
       self.e_page.insert(0, '%d' % (self.page + 1))
       self._pdf_cache()
       self.config(cursor='')
       self.update()

   def _pdf_cache(self):
       self.pdf_images = []
       for p in range(0, self.pages+1, 10):
           images = p2i.convert_from_path(self.doc,
                                          first_page=p, last_page=min(p+10-1, self.pages),
                                          fmt='jpeg', output_folder='my_tmp/pdfcache')
           for i in images:
               self.pdf_images.append(i)

   def _show_page(self):
       self.config(cursor='watch')
       self.update()
       self.cnv_img.delete('all')
       image = self.pdf_images[self.page]
       # eventuale rotazione
       if self.rotation:
           image = image.rotate(self.rotation, expand=True)
       img_x, img_y = image.size
       if self.adapt:
           # calcola i rapporti di scala
           r_img = img_x / img_y
           r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
           if r_cnv <= r_img:
               f_scala = self.cnv_img.winfo_width() / img_x
           else:
               f_scala = self.cnv_img.winfo_height() / img_y
           fx = int(img_x * f_scala)
           fy = int(img_y * f_scala)
           image = image.resize((fx, fy))
           x = self.cnv_img.winfo_width() // 2
           y = self.cnv_img.winfo_height() // 2
           x_region = self.cnv_img.winfo_width()
           y_region = self.cnv_img.winfo_height()
       elif self.zoom != 0.0:
           f_x = int(img_x * (1 + self.zoom))
           f_y = int(img_y * (1 + self.zoom))
           image = image.resize((f_x, f_y))
           x = f_x // 2
           y = f_y // 2
           x_region = f_x
           y_region = f_y
       else:
           x = img_x // 2
           y = img_y // 2
           x_region = img_x
           y_region = img_y
       self.img = ImageTk.PhotoImage(image)
       self.cnv_img.create_image(x, y, image=self.img, anchor='center')
       self.cnv_img.configure(scrollregion=(0, 0, x_region, y_region))
       self.cnv_img.update()
       self.e_page.delete(0, tk.END)
       self.e_page.insert(0, '%d' % (self.page + 1))
       self.e_page.update()
       self.config(cursor='')
       self.update()
   
   def _show_info(self):
       message = ''
       maxsize = 0
       for key in self.info.keys():
           if len(key) > maxsize: maxsize = len(key)
       for key in self.info.keys():
           message += key + ' '*(maxsize-len(key)) + ' : ' + str(self.info[key]) + '\n'
       msgb.showinfo('File aperto con successo', message)



if __name__ == '__main__':
   app = tk.Tk()
   viewer = PdfViewer()
   viewer.set_file('reportlab-sample.pdf')
   app.mainloop()


Se volete riprodurre quanto sopra ricordate dei due sorgenti nel precedente post.

Il prototipo del viewer versione tkinter è completato, il prossimo post stessa cosa, con wxPython ;)

Edited by nuzzopippo - 12/5/2021, 17:29
 
Web  Top
view post Posted on 23/5/2021, 09:44
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


La versione wx


Come di il nome di questa sezione, ciò che scrivo qui sono solo degli appunti di apprendimento, ciò che segue è il mio primissimo tentativo di costruire una finestra applicativa utilizzando il framework wxPython, tentativo in cui mi è riuscito di ottenere questo

png


... Ammesso qualcuno legga questi post, probabilmente si starà domandando perché mai mi metta a "sviluppare" con due framework diversi una stessa applicazione sostanzialmente identica, sarebbe una giusta domanda.
Come un po' tutti gli iniziandi, anche io mi sono imbattuto nel framework tkinter non appena iniziato ad interessarmi a python, in fin dei conti viene fornito di default con il linguaggio e si trovano in rete molti esempi, seppur elementari, mi è stato facile acquisire una rudimentale capacità di implementare qualcosa con tale framework ma nel contempo ho potuto constatare la presenza di due importanti fattori negativi a suo sfavore :
  1. tkinter è nel complesso rudimentale, carente sotto molti aspetti e mancano alcuni widget di uso comune, tipo tabelle etc.;
  2. fattore più importante, la documentazione di tkinter è molto frammantata e difficilmente reperibile, alcune delle principali fonti informative sono "svanite" nel tempo, inoltre (fattore per me importante) vi è ben poco di buono in lingua italiana
Per contro, il progetto Phoenix, attuale versione delle wxPython, mette a disposizione un ambiente molto sofisticato e maturo, anche se magari un po' complesso, con un'ottima documentazione, ben organizzata e che può essere scaricata e consultata in locale, oltre che di una demo decisamente notevole che espone esempi d'uso di numerosi dei molti controlli disponibili.
... per altro, esiste un'ottima risorsa in italiano, il testo "Capire wxPython, dell'ottimo Riccardo Poligneri, certo è un work in progress da completare ma già così com'è è un ottimo mezzo per apprendere.
Or bene, qui si tratta proprio di "apprendere", i framework grafici sono discorsi complessi che si innestano su quell'altro elemento complesso che è il linguaggio Python, possono essere affrontati ma un passo per volta (da qui i "passetti" che caratterizzeranno queste serie di post).
La parte con tkinter si giustifica col tentativo (in cui spero poco) di portare un po' di iniziandi come me a discutere su quanto qui fatto per migliorarlo tanto nelle tecniche quanto nelle funzioni, la parte wxPython è lo studio che in realtà voglio fare per me, anch'essa aperta a discussioni e miglioramenti ... il tutto, senza alcuna fretta

Veniamo al dunque!


Dopo la chilometrica introduzione sopra, passiamo a considerare il pdf-viewer realizzato tramite le wx, in linea di massima la logica funzionale è tutto sommato analoga a quella implementata per la versione tkinter (e non ci si dilungherà in merito), dei bottoni di comando e delle funzioni di callback che eseguono analoghe "operazioni".
Comunque, una prima differenziazione possiamo già notarla all'avvio della applicazione, come esposto nella sezione "Accortezze" del 2° post, la logica applicata in questo visualizzatore prevede che esista, nella direttrice contenente lo script, una struttura di sub-directory "my_tmp/pdfcache" da crearsi a manina. Non è che non possa farsi altrimenti, intendiamoci, ma la classe "Tk()" di tkinter, che lancia il "mainloop()" è a tutti gli effetti una finestra, non è il caso di "sporcarla" con accorgimenti non pertinenti; il punto di avvio del loop di esecuzione delle wx appartiene alla classe "wx.App" che non è una finestra anche se pur essa avvia tramite "MainLoop()" una thread bloccante per la gestione delle GUI, e possiede svariate proprietà e metodi interessanti, in particolare, nel nostro caso, i metodi "OnInit" ed "OnExit", della sub-classe "AppConsole" ci permetteranno di eseguire prima dell'avvio del MainLoop una verifica ed eventuale creazione creazione delle directory, ridefinendo OnInit, oltre alla pulizia di eventuali residui nella cache al termine del MainLoop, ridefinendo OnExit :
CODICE
class PdfApp(wx.App):
   def OnInit(self):
       app_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
       tmp_dir = os.path.join(app_dir, 'my_tmp')
       if not os.path.exists(tmp_dir) or not os.path.isdir(tmp_dir):
           os.mkdir(tmp_dir)
       self.cache_dir = os.path.join(tmp_dir, 'pdf_cache')
       if not os.path.exists(self.cache_dir) or not os.path.isdir(self.cache_dir):
           os.mkdir(self.cache_dir)
       return True

   def OnExit(self):
       files = glob.glob(os.path.join(self.cache_dir, '*.*'))
       for f in files:
           try:
               os.remove(f)
           except OSError:
               pass
       return True


Altro punto, notevole, di differenza tra i due framework grafici è dato dal fatto che wx mantiene in memoria le immagini applicate ai controlli che possono esporle (mentre tkinter non lo fa), cosa che mi ha spinto ad un ozioso esperimento di creazione "automatizzata" dei pulsanti, definendo una tupla con le chiavi delle icone interessanti
CODICE
...
   ico_keys = ('pg_first', 'pg_left', 'pg_right', 'pg_last', 'r_left',
               'r_right', 'zoom_out', 'zoom_in', 'zoom_view',
               'info', 'win_close')
...

e scrivendo un metodo che "automatizzasse" la creazione di bottoni dotati di icona e tooltip e li restituisse
CODICE
...
   def btn_ico_and_tip(self, pnl, keys):
       buttons = []
       for key in keys:
           img = IaT.IcoDispencer().getIco(key)
           bmpimg = wx.Bitmap(wx.Image(BytesIO(base64.b64decode(img))),
                                       depth=wx.BITMAP_SCREEN_DEPTH)
           bt = GBTB(pnl, wx.ID_ANY, bmpimg, name=key)
           bt.SetToolTip(IaT.IcoDispencer().getDescr(key))
           buttons.append(bt)
       return buttons
...

forse, nel codice, avrete notato che i bottoni vengono definiti tramite uno strano oggetto "GBTB", essa altro non è che una mia abbraviazione di comodo per l'oggetto "GenBitmapButton" della libreria wx così abbreviato nell'importarlo.
Come detto, la libreria wx è molto matura e decisamente più ricca di tkinter, lo testimonia anche il binding dei pulsanti, ove è disponibile un evento specializzato "wx.EVT_BUTTON" che permette di non sottilizzare troppo tra eventi di mouse/tastiera.
Per altro anche la metodologia di definizione della immagine è diversa, forse un attimino meno immediata in wx ma di fondo non più complessa.

Per altro, se leggerete il metodo "_populate" del PdfViewer versione xwPython, noterete che i bottoni NON vengono dichiarati quali variabili di istanza, cosa fattibilissima anche in tkinter, si tratta di una metodologia che di solito non utilizzo, qui ho voluto giocarci un po' (sto apprendendo no? ;) ) ma preferisco "istanziare" (diciamo così), certamente scrivo più codice ma mi confondo meno.

Quanto sopra prevede l'utilizzo dello stesso file "viewer_ico_and_tip.py" utilizzato con tkinter ma non dell'altro modulo per la creazione dei tooltip, wx li prevede già da se come proprietà dei suoi widget.

Anche per la rappresentazione dell'immagine della pagina corrente ho utilizzato un oggetto della libreria wx : ScrolledPanel
È un semplice pannello con scrollbar, niente di che, inizialmente mi ci son perso un attimino in prove varie, ma alla fine ho sub-classato un oggetto specializzato a rappresentante una bitmap che bene o male funziona ;)
CODICE
class ScrollBMP(ScrolledPanel):
   def __init__(self, parent, *args, **kwargs):
       super(ScrollBMP, self).__init__(parent, *args, **kwargs)
       self.bmp_sizer = wx.BoxSizer(wx.VERTICAL)
       self.bmp = None
       self.SetSizer(self.bmp_sizer)
       self.SetupScrolling()
       self.Bind(wx.EVT_SIZE, self.on_resize)
       self.FitInside()
   
   def show_bmp(self, bitmap):
       if self.bmp: self.bmp.Destroy()
       self.Scroll(0, 0)
       self.bmp = wx.StaticBitmap(self, bitmap=bitmap)
       self.bmp_sizer.Add(self.bmp, 1, wx.EXPAND)
   
   def show_pil_img(self, image):
       wx_img = wx.Image(image.width, image.height)
       self.SetVirtualSize(image.width, image.height)
       wx_img.SetData(image.convert('RGB').tobytes())
       bitmap = wx.Bitmap(wx_img)
       self.show_bmp(bitmap)
   
   def on_resize(self, evt):
       self.Layout()


Una differenza "importante" tra wx e tkinter è che mentre il layout delle finestre in tkinter si basa sui gestori di geometri (grid, pack, place) il layout in wx si basa sui "Sizer" di cui fornisce una variegata pleora di classi, oggetti articolati e che richiedono un po' di prove e tempo per capirli ed usarli decentemente (hai non anglofoni suggerisco una lettura del libro di Poligneri in link all'inizio), quanto prodotto funziona "come volevo" ma, certo, può essere migliorato.

Per altro, anche in wx le finestre di dialogo di default in wx mi lasciano un po' insoddisfatto

png


Qui le righe di testo non vanno a capo a c...o come in tkinter ma il font non mi piace! ... vedremo in seguito.

Intanto il codice completo per la versione wx di PdfViewer:
CODICE
# -*- coding: utf-8 -*-

import wx
from wx.lib.buttons import GenBitmapButton as GBTB
from wx.lib.scrolledpanel import ScrolledPanel
import viewer_ico_and_tip as IaT
import pdf2image as p2i
import sys
import os
import glob
from PIL import Image
from io import BytesIO
import base64


class ScrollBMP(ScrolledPanel):
   def __init__(self, parent, *args, **kwargs):
       super(ScrollBMP, self).__init__(parent, *args, **kwargs)
       self.bmp_sizer = wx.BoxSizer(wx.VERTICAL)
       self.bmp = None
       self.SetSizer(self.bmp_sizer)
       self.SetupScrolling()
       self.Bind(wx.EVT_SIZE, self.on_resize)
       self.FitInside()
   
   def show_bmp(self, bitmap):
       if self.bmp: self.bmp.Destroy()
       self.Scroll(0, 0)
       self.bmp = wx.StaticBitmap(self, bitmap=bitmap)
       self.bmp_sizer.Add(self.bmp, 1, wx.EXPAND)
   
   def show_pil_img(self, image):
       wx_img = wx.Image(image.width, image.height)
       self.SetVirtualSize(image.width, image.height)
       wx_img.SetData(image.convert('RGB').tobytes())
       bitmap = wx.Bitmap(wx_img)
       self.show_bmp(bitmap)
   
   def on_resize(self, evt):
       self.Layout()


class PdfViewer(wx.Frame):
   ico_keys = ('pg_first', 'pg_left', 'pg_right', 'pg_last', 'r_left',
               'r_right', 'zoom_out', 'zoom_in', 'zoom_view',
               'info', 'win_close')
   def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.SetTitle('wxPDFViewer >= Nessun File')
       self.doc = ''
       self.pages = 0
       self.page = 0
       self.pdf_images = []
       self.adapt = True
       self.zoom = 0.0
       self.rotation = 0

       self._populate()
       self.Refresh()
   
   def _populate(self):
       self.SetTitle('wxPDFViewer')
       bg_sizer = wx.BoxSizer(wx.VERTICAL)
       p = wx.Panel(self, style=wx.RAISED_BORDER)
       bts = self.btn_ico_and_tip(p, self.ico_keys)
       bt_sizer = wx.FlexGridSizer(1, 18, 5, 5)
       # navigazione pagine
       lbl = wx.StaticText(p, label='Pagina:')  # column 0
       #bt_sizer.Add(0, 5)
       bt_sizer.Add(lbl, 0, wx.ALIGN_CENTER, border=5)
       bt_sizer.Add(bts[0], border=5)  # column 1
       bt_sizer.Add(bts[1], border=5)  # column 2
       self.t_page = wx.TextCtrl(p, name='e_page')
       l_size = self.t_page.GetSizeFromTextSize(self.t_page.GetTextExtent('99999'))
       self.t_page.SetSize(l_size)
       bt_sizer.Add(self.t_page, 0, wx.ALIGN_CENTER, border=5)  # column 3
       self.lbl_pages = wx.StaticText(p, label='di 100.000')
       bt_sizer.Add(self.lbl_pages, 0, wx.ALIGN_CENTER, border=5)  # column 4
       bt_sizer.Add(bts[2], border=5)  # column 5
       bt_sizer.Add(bts[3], border=5)  # column 6
       bt_sizer.Add(wx.StaticText(p), 0, wx.EXPAND, border=5)  # column 7
       bt_sizer.Add(bts[4], border=5)  # column 8
       bt_sizer.Add(bts[5], border=5)  # column 9
       bt_sizer.Add(wx.StaticText(p), 0, wx.EXPAND, border=5)  # column 10
       bt_sizer.Add(bts[6], border=5)  # column 11
       bt_sizer.Add(bts[7], border=5)  # column 12
       bt_sizer.Add(bts[8], border=5)  # column 13
       bt_sizer.Add(wx.StaticText(p), 0, wx.EXPAND, border=5)  # column 14
       bt_sizer.Add(bts[9], border=5)  # column 15
       bt_sizer.Add(wx.StaticText(p), 0, wx.EXPAND, border=5)  # column 16
       bt_sizer.Add(bts[10], border=5)  # column 17
       p.SetSizer(bt_sizer)
       bt_sizer.AddGrowableCol(7, 1)
       bt_sizer.AddGrowableCol(10, 1)
       bt_sizer.AddGrowableCol(14, 1)
       bt_sizer.AddGrowableCol(16, 1)
       bg_sizer.Add(p, 0, wx.EXPAND|wx.HORIZONTAL)
       self.p_img = ScrollBMP(self, size=wx.Size(-1,400))
       bg_sizer.Add(self.p_img, 1, wx.EXPAND, border=10)
       self.SetSizer(bg_sizer)
       bg_sizer.Fit(self)
       self.Fit()

       dim = self.GetSize()
       self.SetMinSize(dim)

       # BINDINGS e altro
       for i in range(4):
           bts[i].Bind(wx.EVT_BUTTON, self._set_page)
       self.t_page.Bind(wx.EVT_KEY_DOWN, self._set_page)
       bts[4].Bind(wx.EVT_BUTTON, self._rotate)
       bts[5].Bind(wx.EVT_BUTTON, self._rotate)
       # messi qui per controllo progressione
       self.bt_zoom_out = bts[6]
       self.bt_zoom_out.Bind(wx.EVT_BUTTON, self._set_zoom)
       self.bt_zoom_out.Disable()
       self.bt_zoom_in = bts[7]
       self.bt_zoom_in.Bind(wx.EVT_BUTTON, self._set_zoom)
       self.bt_zoom_in.Disable()
       bts[8].Bind(wx.EVT_BUTTON, self._adapt)
       bts[9].Bind(wx.EVT_BUTTON, self._show_info)
       bts[10].Bind(wx.EVT_BUTTON, self._on_closing)

       self.Bind(wx.EVT_SIZE, self._on_resize)
       self.Bind(wx.EVT_CLOSE, self._on_closing)

   def btn_ico_and_tip(self, pnl, keys):
       buttons = []
       for key in keys:
           img = IaT.IcoDispencer().getIco(key)
           bmpimg = wx.Bitmap(wx.Image(BytesIO(base64.b64decode(img))),
                                       depth=wx.BITMAP_SCREEN_DEPTH)
           bt = GBTB(pnl, wx.ID_ANY, bmpimg, name=key)
           bt.SetToolTip(IaT.IcoDispencer().getDescr(key))
           buttons.append(bt)
       return buttons

   # *** CALLBACK BINDING ***
   def _on_resize(self, evt):
       if self.doc and self.pdf_images and self.adapt:
           self._show_page()
       evt.Skip()

   def _on_closing(self, evt):
       self._clear_cache()
       self.Destroy()

   def _set_page(self, evt):
       if not self.doc or not self.pdf_images:
           evt.Skip()
           return
       w_obj = evt.GetEventObject()
       w_name = w_obj.GetName()
       if w_name == 'e_page':
           key = evt.GetKeyCode()
           if not key == wx.WXK_RETURN and not key == wx.WXK_NUMPAD_ENTER:
               evt.Skip()
               return
           try:
               value = int(w_obj.GetValue())
               if 0 <= (value -1) <= self.pages -1:
                   self.page = value -1
               else:
                   raise ValueError('Inserimento non valido')
           except ValueError:
               message = 'Numero di pagina non valido'
               wx.MessageBox(message, 'Set Pagina', wx.OK|wx.ICON_ERROR)
               return
       elif w_name == 'pg_first':
           self.page = 0
       elif w_name == 'pg_left':
           self.page -= 1
           if self.page < 0: self.page = 0
       elif w_name == 'pg_right':
           self.page += 1
           if self.page > self.pages -1: self.page = self.pages -1
       elif w_name == 'pg_last':
           self.page = self.pages -1
       else:
           evt.Skip()
           return
       self._show_page()

   def _adapt(self, evt):
       self.adapt = not self.adapt
       if self.adapt:
           self.zoom = 0.0
           self.bt_zoom_out.Disable()
           self.bt_zoom_in.Disable()
       else:
           self.bt_zoom_out.Enable()
           self.bt_zoom_in.Enable()
       self._show_page()

   def _set_zoom(self, evt):
       if not self.doc or not self.pdf_images:
           evt.Skip()
           return
       w_obj = evt.GetEventObject()
       w_name = w_obj.GetName()
       if w_name == 'zoom_out':
           self.zoom -= 0.1
           if self.zoom < -0.9:
               self.zoom = -0.9
               return
       elif w_name == 'zoom_in':
           self.zoom += 0.1
           if self.zoom > 3.00:
               self.zoom = 3.00
               return
       self._show_page()

   def _rotate(self, evt):
       if not self.doc or not self.pdf_images:
           evt.Skip()
           return
       w_obj = evt.GetEventObject()
       w_name = w_obj.GetName()
       if w_name == 'r_left':
           self.rotation += 90
       elif w_name == 'r_right':
           self.rotation -= 90
       if self.rotation > 270:
           self.rotation = 0
       elif self.rotation < 0:
           self.rotation = 270
       self._show_page()


   def _show_info(self, evt):
       message = ''
       maxsize = 0
       for key in self.info.keys():
           if len(key) > maxsize: maxsize = len(key)
       for key in self.info.keys():
           message += key + ' '*(maxsize-len(key)) + ' : ' + str(self.info[key]) + '\n'
       wx.MessageBox(message, 'PDFInfo',
           wx.OK | wx.ICON_INFORMATION)
   
   # *** METODI ***
   def _clear_cache(self):
       files = glob.glob('my_tmp/pdf_cache/*.*')
       for f in files:
           try:
               os.remove(f)
           except OSError:
               pass


   def set_file(self, f_pathname):
       if not os.path.exists(f_pathname) or not os.path.isfile(f_pathname):
           return
       self._clear_cache()
       self.doc = f_pathname
       f_name = os.path.basename(f_pathname)
       self.SetTitle('wxPDFViewer => ' + f_name)
       self.SetCursor(wx.Cursor(wx.CURSOR_WAIT))
       self._clear_cache()
       self.info = p2i.pdfinfo_from_path(self.doc)
       self.pages = self.info['Pages']
       self.lbl_pages.SetLabel('di %d'%self.pages)
       self.page = 0
       self.t_page.SetValue('%d' % (self.page + 1))
       self._pdf_cache()
       self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))
       self._show_page()

   def _pdf_cache(self):
       self.pdf_images = []
       for p in range(0, self.pages+1, 10):
           images = p2i.convert_from_path(self.doc,
                                          first_page=p, last_page=min(p+10-1, self.pages),
                                          fmt='jpeg', output_folder='my_tmp/pdf_cache')
           for i in images:
               self.pdf_images.append(i)

   def _show_page(self):
       self.SetCursor(wx.Cursor(wx.CURSOR_WAIT))
       image = self.pdf_images[self.page]
       # eventuale rotazione
       if self.rotation:
           image = image.rotate(self.rotation, expand=True)
       img_x, img_y = image.size
       if self.adapt:
           # calcola i rapporti di scala
           r_img = img_x / img_y
           #r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
           r_cnv = self.p_img.GetSize()[0] / self.p_img.GetSize()[1]
           if r_cnv <= r_img:
               f_scala = self.p_img.GetSize()[0] / img_x
           else:
               f_scala = self.p_img.GetSize()[1] / img_y
           fx = int(img_x * f_scala)
           fy = int(img_y * f_scala)
           image = image.resize((fx, fy))
       elif self.zoom != 0.0:
           f_x = int(img_x * (1 + self.zoom))
           f_y = int(img_y * (1 + self.zoom))
           image = image.resize((f_x, f_y))
       self.p_img.show_pil_img(image)
       self.t_page.SetValue('%d' % (self.page + 1))
       self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))





class PdfApp(wx.App):
   def OnInit(self):
       app_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
       tmp_dir = os.path.join(app_dir, 'my_tmp')
       if not os.path.exists(tmp_dir) or not os.path.isdir(tmp_dir):
           os.mkdir(tmp_dir)
       self.cache_dir = os.path.join(tmp_dir, 'pdf_cache')
       if not os.path.exists(self.cache_dir) or not os.path.isdir(self.cache_dir):
           os.mkdir(self.cache_dir)
       return True

   def OnExit(self):
       files = glob.glob(os.path.join(self.cache_dir, '*.*'))
       for f in files:
           try:
               os.remove(f)
           except OSError:
               pass
       return True


if __name__ == '__main__':
   app = PdfApp()
   frame = PdfViewer(parent=None)
   frame.Show()
   frame.set_file('reportlab-sample.pdf')
   app.MainLoop()


Questa serie di post finisce qui, per il prossimo passetto ho intenzione di affrontare l'estrazione del testo da pdf, tanto ordinari quanto da scansione (immagini), a singolo documento, per poi affrontare la creazione di csv scatenante questi post ... sarà un passo intermedio.
 
Web  Top
3 replies since 2/5/2021, 07:47   618 views
  Share