Schriftbreite im Eingabefeld ermitteln

Fragen zu Tkinter.
Antworten
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Hallo,

ich will in einer (einem?) GUI ein Entry anbieten, in das ein Spielername eingegeben werden kann. Zur Zeit wird auf 8 Zeichen begrenzt. Soweit klappt auch alles.

Allerdings hat das Ausgabefeld nur eine bestimmte Breite. Das Feld sollte möglichst gut ausgenutzt werden können.
Auf Grund unterschiedlicher Buchstabenbreiten kann ja nun eine unterschiedliche Stringlänge dargestellt werden. Wie ermittle ich die grafische Breite des Strings?
Im Ergebnis gäbe es zwei Möglichkeiten zu reagieren. Entweder die Schriftgröße soweit verringern, dass der String in das Ausgabefeld passt oder die Anzahl der Zeichen in Abhängigkeit von den verwendeten Buchstaben soweit begrenzen, dass mit einer festen Schriftgröße der eingegeben String vollständig dargestellt werden kann.

Hat jemand eine Idee?
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

So ganz verstehe ich dein Problem nicht. Da du nicht weißt, wie der Name ist, kannst du doch eh nur 8*breitester Buchstabe machen. Alles andere wäre ein rumzuckender Albtraum.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Die aktuell 8 Zeichen habe ich nur eingebaut, um erstmal die meisten Eingaben abfangen und den eingegebenen String im Ausgabefeld halbwegs vernünftig darstellen zu können. Das klappt auch in wahrscheinlich 90 % der Fälle. Extremabweichungen wie "WWWWWWWW" oder "iiiiiiii" sehen aber unschön aus bzw. passen dann doch nicht.
Im Idealfall würde ich wahrscheinlich eine feste Anzahl Zeichen zulassen und den String dann über Scalierung der Schriftgröße an das Ausgabefeld anpassen wollen.
Ich habe inzwischen auch schon was dazu gefunden, mit dem man wohl die Breite in Pixeln ermitteln kann. Stichwort font.measure(text). Ich muss jetzt halt nur noch schauen, wie man das am besten anwendet.

Das mit dem rumzappeln würde ich hinnehmen, bzw. ist denke ich überschaubar.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Tu was du nicht lassen kannst. Als Anwender (besonders mit mobilen Geräten) kotze ich im Strahl. Du legst hier einen seltsamen Sinn für Ästhetik an. Ein Eingabeelement, das sich bei Benutzung verändert ist in meinen Augen ein no Go.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Wie bereits beschrieben, werde ich wahrscheinlich nicht die Eingabe, sondern die Ausgabe anpassen. Bei der Eingabe wird nur die Länge begrenzt bzw. die Eingabe weiterer Zeichen verhindert. Soll auch keine App fürs Handy werden, sondern wird für eine Anwendung auf einem Rasp genutzt werden.

Hätt ja sein können, dass jemand schon eine Idee bzw. ein Beispiel hätte, wie man die Pixelbreite eines Strings ermittelt.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wie das geht hängt vom Toolkit ab. Qt kann das zb. Aber die anderen sollten auch was haben. Welches verwendest du?
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi suk

Habe hier etwas zusammengebastelt:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import tkinter as tk
import tkinter.font as fnt

APP_TITLE = "String Pixle Length"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 700
APP_HEIGHT = 200

        
class Application(tk.Tk):

    def __init__(self):
        tk.Tk.__init__(self)
        self.title(APP_TITLE)
        self.build()

    def build(self):
        self.entry_font = fnt.Font(family='Helvetica', size=10, weight='bold')
        self.label_font = fnt.Font(family='Helvetica', size=18, weight='bold')
        
        self.protocol("WM_DELETE_WINDOW", self.close_app)
        self.geometry("+{}+{}".format(APP_XPOS, APP_YPOS))
        self.geometry("{}x{}".format(APP_WIDTH, APP_HEIGHT))
        self.option_add("*Button.highlightThickness", 0)

        self.main_frame = tk.Frame(self, bg='yellow', bd=0)
        self.main_frame.pack(side='left', fill='both', expand=True)
        
        self.entry_container = tk.Frame(self.main_frame, bd=0)
        self.entry_container.pack(expand=True)
        self.entry_container.propagate(False)
        
        self.entry_var = tk.StringVar()
        self.entry = tk.Entry(self.entry_container, font=self.entry_font,
            textvariable=self.entry_var, highlightthickness=0, bd=0)
        self.entry.pack(fill='x')
            
        self.label_container = tk.Frame(self.main_frame, bd=0)
        self.label_container.pack(expand=True)
        self.label_container.propagate(False)
       
        self.label_var = tk.StringVar()
        self.label = tk.Label(self.label_container, textvariable=self.entry_var,
            font=self.label_font, bg='white')
        self.label.pack()
        
        self.entry_var.trace('w', self.var_callback)
        self.entry_string = "WWWWWWWW" #"iiiiiiii" #
        self.entry_var.set(self.entry_string)
        
    def var_callback(self, *args):
        
        string_length = self.entry_font.measure(self.entry_var.get())
        string_height = self.entry_font.metrics('linespace')
        self.entry_container.config(width=string_length+2, height=string_height)

        string_length = self.label_font.measure(self.entry_var.get())
        string_height = self.label_font.metrics('linespace')
        self.label_container.config(width=string_length+2, height=string_height)
        
    def close_app(self):
        # Here do something before apps shutdown
        print("Good Bye!")
        self.destroy()

              
Application().mainloop()
Gruss wuf :wink:
Take it easy Mates!
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Danke wuf ...

Habe inzwischen auch ein *schnipplet* gefunden und mit diesem mal selber ein bisschen probiert.
Ist zwar sicher nicht sehr sauber programmiert, sollte jedoch die Funktion etwas veranschaulichen.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

### Import der erforderlichen Module ###
try:
# Tkinter for Python 2.xx
    import Tkinter as tk
except ImportError:
# Tkinter for Python 3.xx
    import tkinter as tk
    
from functools import partial
import tkinter.font as tkf

### Fensterdefinition ###
APP_TITLE = "Template_Canvas"
APP_WIDTH = 600
APP_HEIGHT = 300

### Inhalt Canvas-Applikation ###
class App_Canvas():
    def __init__(self, canvasmaster):
        self.mycanvas = canvasmaster
#        print(self.mycanvas)
        self.measure_beispiel("yellow world")
        self.measure_anwendung()

### Beispiel für eine Berechnung pixelgenaue Textbreite und -höhe
    def measure_beispiel(self, text):
        (x,y) = (20,20)
        fonts = []
        for (family, size) in [("Arial", 20), ("Times", 20)]:
            font = tkf.Font(family=family, size=size)
            (breite, hoehe) = (font.measure(text), font.metrics("linespace"))
            print("%s %s: (%s,%s)" % (family, size, breite, hoehe))
            self.mycanvas.create_text(x,y,text="_%s_" % (text), font=(family, size),anchor="nw")
            self.mycanvas.create_text(x,y + hoehe + 5,text="%s: BxH=%s" % ((family, size), (breite, hoehe)),font=("Arial", 10),anchor="nw")
            y += hoehe + 25

### und ein Beispiel für die Nutzung in einer Anwendung
    def measure_anwendung(self):
        self.text = "Name 1"
        self.a_x = 150
        self.a_y = 100
        self.a_font = "Arial"
        self.a_font_size = 15
        obj_frame = tk.Frame(self.mycanvas, background="gray", bd=3, relief="groove", width=300, height=150)
        self.eingabefeld = tk.Entry(obj_frame, relief = "sunken",  borderwidth=2, font=("Arial", 15))
        self.eingabefeld.insert(0, self.text)
        self.eingabefeld.select_range(0, len(self.eingabefeld.get()))
        self.eingabefeld.focus()
        self.eingabefeld.place(x=10, y=5, width=270, height=30)
        self.eingabefeld.bind("<KeyRelease>", self.event_keyrelease)
        self.eingabefeld.bind("<FocusOut>", self.event_lostfocus)
        self.ausgabefeld = tk.Label(obj_frame, relief = "groove", font=(self.a_font, self.a_font_size), bg="lightgray")
        self.ausgabefeld.place(x=10, y=40, width=self.a_x, height=self.a_y)
        self.mycanvas.create_window(20, 150, window=obj_frame, anchor="nw")

    def event_keyrelease(self, event):
#        print(event.keycode)
        maxchar = 20
        if (int(len(self.eingabefeld.get()) > maxchar)):
# Länge auf maximale Anzahl Zeichen zurücksetzen
            print("maximale Anzahl Zeichen erreicht")
            self.eingabefeld.delete(maxchar, len(self.eingabefeld.get()))
#            self.eingabefeld.xview_moveto(0)
        if (event.keycode == 9):
# ESC-Taste
            print("Abbruch durch Escape")
            self.ausgabefeld.config(text = "")
            self.eingabefeld.config(text = self.eingabefeld.get())
        if (event.keycode == 36):
# Enter-Taste            
            print("\neingegebener Text ist: %s" % (self.eingabefeld.get()))
# anpassen der Schriftgröße
            var_font = tkf.Font(family=self.a_font, size=int(self.a_font_size))
            var_text = self.eingabefeld.get()
# textmaße ermitteln
            var_textlaenge = var_font.measure(var_text)
            var_texthoehe = var_font.metrics("linespace")
# Verhältnis zum ausgabefeld ermitteln
            var_verhaeltnis = self.a_x / var_textlaenge
            print("Verhältnis Textfeldbreite zu Textbreite: {} / {} = {}".format(self.a_x, var_textlaenge, var_verhaeltnis))
# maximale schriftgröße ermitteln, so dass der Text in das Ausgabefeld passen würde
            var_font_size_neu = int(self.a_font_size * var_verhaeltnis * 0.95)
            print("Schriftgröße alt: {}, neu: {}".format(self.a_font_size, var_font_size_neu))
            var_font_neu = tkf.Font(family=self.a_font, size=var_font_size_neu)
            print("neu: (Textbreite, Textfeldbreite) (Texthöhe, Textfeldhöhe): ({}, {}) ({}, {})".format(var_font_neu.measure(var_text), self.a_x, var_font_neu.metrics("linespace"), self.a_y))
# wenn Ergenbis Texthöhe zu hoch, Schriftgröße nochmal verringern
            if (var_font_neu.metrics("linespace") > self.a_y):
                var_verhaeltnis = self.a_y / var_font_neu.metrics("linespace")
                print("Verhältnis Textfeldhöhe zu Texthöhe: {} / {} = {}".format(self.a_y, var_font_neu.metrics("linespace"), var_verhaeltnis))
                var_font_size_neu = int(var_font_size_neu * var_verhaeltnis)
                print("Schriftgröße alt: {}, neu: {}".format(self.a_font_size, var_font_size_neu))
                var_font_neu = tkf.Font(family=self.a_font, size=var_font_size_neu)
                print("neu: (Textbreite, Textfeldbreite) (Texthöhe, Textfeldhöhe): ({}, {}) ({}, {})".format(var_font_neu.measure(var_text), self.a_x, var_font_neu.metrics("linespace"), self.a_y))
            self.ausgabefeld.config(text = self.eingabefeld.get(), font = var_font_neu)
            self.text = self.eingabefeld.get()

    def event_lostfocus(self, event):
# Eingabefeld hat Focus verloren
        print("Abruch durch Focusverlust")
        self.ausgabefeld.config(text = "")


### Inhalt Frame-Applikation ###
class App_Frame1():
    def __init__(self, obj_frame):
        self.myframe = obj_frame


### Applikationsstart ###
def main():
    win_master = tk.Tk()
    win_master.title(APP_TITLE)
    app_canvas = tk.Canvas(win_master, width=APP_WIDTH, height=APP_HEIGHT, relief="groove", bg="white", bd=5)
    app_canvas.pack()
#    app_frame = tk.Frame(win_master, width=APP_WIDTH, height=APP_HEIGHT, relief="sunken", bg="gray", bd=3)
#    app_frame.pack()
    button1 = tk.Button(win_master, text="close", command=win_master.destroy)
    button1.pack()
    App_Canvas(app_canvas)
#    App_Frame(app_frame)
    win_master.mainloop()

if __name__ == '__main__':
    main()
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi suk

Dein Skript war sehr schwer zu lesen obwohl es in Python geschrieben ist. Konnte aber mit Hilfe des Skripts besser verstehen was du eigentlich erreichen möchtest. Habe hier meine eigene Variante erstellt:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import tkinter as tk
import tkinter.font as tkf

APP_TITLE = "TkTemplate_Small"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 600
APP_HEIGHT = 310


class Application(tk.Canvas):

    def __init__(self, app_win, **kwargs):
        self.app_win = app_win
        app_win.protocol("WM_DELETE_WINDOW", self.close_app)
        
        tk.Canvas.__init__(self, app_win, **kwargs)
        
    def build(self):
        self.measure_samples("yellow world")
        self.output_font = tkf.Font(family='Helvetica', size=10)
        # Subframe
        self.entry_frame = tk.Frame(self, bg='gray', bd=3, relief="groove",
            width=300, height=150)
        
        # Eingabefeld        
        self.entry_var = tk.StringVar()
        self.max_char = 8
        self.entry_var.trace("w", self.entry_callback)
        
        self.entry = tk.Entry(self.entry_frame, relief = "sunken", bd=2,
            textvariable=self.entry_var, font=("Arial", 15))
        self.entry.place(x=10, y=5, width=270, height=30)
        self.entry_var.set("Name1")
        self.entry.select_range(0, len(self.entry.get()))
        self.entry.focus()
        
        self.entry.bind("<KeyRelease>", self.event_key_release)
        self.entry.bind("<FocusOut>", self.event_lost_focus)
        
        # Ausgabefeld
        label_width = 150
        label_height = 100
        self.label_var = tk.StringVar()  
        self.label_display = tk.Label(self.entry_frame, relief="groove", bd=2, 
            font=('Arial', 15), bg="lightgray",
            textvariable=self.label_var)
        self.label_display.place(x=10, y=40, width=label_width,
            height=label_height)
            
        self.create_window(20, 150, window=self.entry_frame, anchor="nw")

        tk.Button(self.app_win, text="close",
            command=self.app_win.destroy).pack()
    
    def entry_callback(self, *args):
        char = self.entry_var.get()[0:self.max_char]
        print("c=" , char)
        self.entry_var.set(char)

    def event_key_release(self, event):
        if event.keysym == 'Return':
            label_width = self.label_display.winfo_width()
            entry_text = self.entry_var.get()
            output_text_width = self.output_font.measure(entry_text)
            
            if output_text_width < label_width:
                adjust = 'larger'
            else:
                adjust = 'smaller'
                
            while True:
                output_text_width = self.output_font.measure(entry_text)
                font_size = self.output_font.cget('size')
                if adjust == 'larger':
                    if output_text_width < label_width:
                        font_size += 1
                    else:
                        break
                elif adjust == 'smaller':
                    if output_text_width > label_width:
                        font_size -= 1
                    else:
                        break
                self.output_font.configure(size=font_size)

            print(label_width, output_text_width, font_size)
            self.label_display.config(font=self.output_font)
            self.label_var.set(entry_text)
            
            
    def event_lost_focus(self, event):
        print('Lost Focus')
        
    def measure_samples(self, text):
        xpos, ypos = (20,20)
        fonts = (
            tkf.Font(family='Helvetica', size=20),
            tkf.Font(family='Times', size=20))
            
        for font in fonts:
            font_fam, size, breite, height = (
                font.cget('family'),
                font.cget('size'),
                font.measure(text),
                font.metrics("linespace"),
                )
            self.create_text(xpos, ypos, text="_{}_".format(text), font=font,
                anchor="nw")
            self.create_text(xpos, ypos + height + 5,
                text="{} {}: BxH=({},{})".format(font_fam, size, breite, height),
                font=("Arial", 10),anchor="nw")
            ypos += height + 25
            
    def close_app(self):
        # Here do something before apps shutdown
        print("Good Bye!")
        self.app_win.destroy()

        
def main():
    app_win = tk.Tk()
    app_win.title(APP_TITLE)
    app_win.option_add("*highlightThickness", 0)
    
    app = Application(app_win, width=APP_WIDTH, height=APP_HEIGHT,
        relief="groove", bg="white", bd=2)
    app.pack()
    app.build()
    
    app_win.mainloop()
 
 
if __name__ == '__main__':
    main()
Gruss wuf :wink:
Take it easy Mates!
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Hallo wuf,

Schon mal Danke für den Vorschlag zum Umbau.

Die while-Schleife scheint mir jedoch bedenklich zu sein.
Zum einen ist sie recht langsam und außerdem hängt sie sich auf, wenn keine Eingabe erfolgt.
Dem werde ich wahrscheinlich die Berechnung per Verhältnis vorziehen.
Die Berücksichtigung der Höhe müsste man auch noch beachten.

Da ich noch recht neu mit Python unterwegs bin, habe ich noch einige Wissenslücken.
Was macht self.entry_var.trace ? Wie bekommst Du es hin, dass ein eingegebenes Zeichen größer max_char gar nicht erst angezeigt wird?

Ich habe außerdem noch ein Verständnisproblem zu Euren Programmstrukturen ...
Wenn ich es richtig verstanden habe, ist app_win eine Instanz von tk.Tk und sollte alle Module kennen.
self.app_win ist eine Referenz auf app_win.
Warum muss tk.Canvas über die Klassendefinition noch zusätzlich vererbt werden? Eigentlich müsste die Klasse doch Canvas über app_win bereits kennen?!
Benutzeravatar
wuf
User
Beiträge: 1529
Registriert: Sonntag 8. Juni 2003, 09:50

Hi suk
Die while-Schleife scheint mir jedoch bedenklich zu sein.
Vor allem was eventuell die Abarbeitungszeit anbelangt. Die Texthöhe habe ich leider nicht berücksichtigt. Der 'while' Hänger kann behoben werden mit:
if len(var_text) == 0: return

Übrigens wirft dieser Fall bei deinem Skript in Zeile 82 die Exception:
var_verhaeltnis = self.a_x / var_textlaenge
ZeroDivisionError: division by zero
Was macht self.entry_var.trace ? Wie bekommst Du es hin, dass ein eingegebenes Zeichen größer max_char gar nicht erst angezeigt wird?
Bei Verwendung einer Kontrollvariabel wird schon ein Event ausgelöst, wenn sie verändert wird bevor die Änderung ins Textfeld übertragen wird. Mit der Methode 'trace'kannst du somit etwas abfangen bevor es im Texteingabefeld erscheint. In unserem Fall wird der Eingabestring auf die vorgegebene Länge MAX_ENTRY_CHAR gestutzt mit dem callback:

Code: Alles auswählen

def entry_trace_callback(self, *args):
    char = self.entry_var.get()[0:MAX_ENTRY_CHAR]
    self.entry_var.set(char)
Du könntest dies auch erreichen durch auslösen eines:
self.entry.bind("<KeyPress>", self.event_key_press)

Code: Alles auswählen

def event_key_press(self, event):
    char = self.entry_var.get()[0:self.max_char]
    self.entry_var.set(char)
Hiermit wird der Text direkt bei der Eingabe über die Tastatur gestutzt. Es passiert so schnell, dass du das Zeichen, welches die maximale Länge übersteigt im Texteingabefeld gar nicht siehst.

Der Unterschied zwischen diesen beinden Varianten ist folgender:
Fall-1: Beim setzen der Kontrollvariablen wird ein überlanger Textstring sofort gestutzt und erst dann im Texteingabefeld angezeigt.
Fall-2: Beim setzen des Texteingabefeldeigenen 'set' Methode wird aber ein überlanger Textstring mit seine voller Länge angezeigt und erst beim drücken einer Tastaturtaste gestutzt.

Noch ein Tipp. In der Methode event_keyrelease würde ich an stelle von keycode eher keysym verwenden. Dann würde der Kode wie folgt lesbarer:
if (event.keycode == 9): = Escape
if (event.keycode == 36): = Return


if (event.keysym == 'Escape'):
if (event.keysym == 'Return'):


Was bezweckst du in Zeile 71 mit dieser Textrückkopplung?
self.eingabefeld.config(text = self.eingabefeld.get())

Noch weiter Tipps:
a) Versuche die PEP8 Richtlinen zu befolgen
b) Codezeilenlänge möglichts maximal auf Seitenbreite begrenzen (Seite im Hochformat).
Überlange Kodezeilen können mit einem \ gekürzt werden
c) Ein Skript möglichts ohne Überlange print-Anweisungen ins Forum platzieren
d) Im Forum möglichst die nummerierten Code Tags benutzen

Habe für die Bestimmung der Textlänge in meinem Skript den Kodeabschnitt aus deinem Skript (weil er besser ist) abgeändert übernommen. Um zu verstehen was in diesem Kodabschnitt abläuft decodierte ich in von:

Code: Alles auswählen

    def event_keyrelease(self, event):
#        print(event.keycode)
        maxchar = 8
        if (int(len(self.eingabefeld.get()) > maxchar)):
# Länge auf maximale Anzahl Zeichen zurücksetzen
            print("maximale Anzahl Zeichen erreicht")
            self.eingabefeld.delete(maxchar, len(self.eingabefeld.get()))
#            self.eingabefeld.xview_moveto(0)
        if (event.keycode == 9):
# ESC-Taste
            print("Abbruch durch Escape")
            self.ausgabefeld.config(text = "")
            self.eingabefeld.config(text = self.eingabefeld.get())
        if (event.keycode == 36):
# Enter-Taste           
            print("\neingegebener Text ist: %s" % (self.eingabefeld.get()))
# anpassen der Schriftgröße
            var_font = tkf.Font(family=self.a_font, size=int(self.a_font_size))
            var_text = self.eingabefeld.get()
# textmaße ermitteln
            var_textlaenge = var_font.measure(var_text)
            var_texthoehe = var_font.metrics("linespace")
# Verhältnis zum ausgabefeld ermitteln
            var_verhaeltnis = self.a_x / var_textlaenge
            print("Verhältnis Textfeldbreite zu Textbreite: {} / {} = {}".format(self.a_x, var_textlaenge, var_verhaeltnis))
# maximale schriftgröße ermitteln, so dass der Text in das Ausgabefeld passen würde
            var_font_size_neu = int(self.a_font_size * var_verhaeltnis * 0.95)
            print("Schriftgröße alt: {}, neu: {}".format(self.a_font_size, var_font_size_neu))
            var_font_neu = tkf.Font(family=self.a_font, size=var_font_size_neu)
            print("neu: (Textbreite, Textfeldbreite) (Texthöhe, Textfeldhöhe): ({}, {}) ({}, {})".format(var_font_neu.measure(var_text), self.a_x, var_font_neu.metrics("linespace"), self.a_y))
# wenn Ergenbis Texthöhe zu hoch, Schriftgröße nochmal verringern
            if (var_font_neu.metrics("linespace") > self.a_y):
                var_verhaeltnis = self.a_y / var_font_neu.metrics("linespace")
                print("Verhältnis Textfeldhöhe zu Texthöhe: {} / {} = {}".format(self.a_y, var_font_neu.metrics("linespace"), var_verhaeltnis))
                var_font_size_neu = int(var_font_size_neu * var_verhaeltnis)
                print("Schriftgröße alt: {}, neu: {}".format(self.a_font_size, var_font_size_neu))
                var_font_neu = tkf.Font(family=self.a_font, size=var_font_size_neu)
                print("neu: (Textbreite, Textfeldbreite) (Texthöhe, Textfeldhöhe): ({}, {}) ({}, {})".format(var_font_neu.measure(var_text), self.a_x, var_font_neu.metrics("linespace"), self.a_y))
            self.ausgabefeld.config(text = self.eingabefeld.get(), font = var_font_neu)
            self.text = self.eingabefeld.get()
um in besser lesen zu können:

Code: Alles auswählen

self.text = "Name 1"
self.output_field.width = 150
self.output_field.height = 100
self.output_field_font = "Arial"
self.output_field_font_size = 15

if (event.keycode == 36):
    # anpassen der Schriftgröße
    var_font = tkf.Font(family=self.output_field_font, size=int(
        self.output_field_font_size))
    var_text = self.eingabefeld.get()

    # textmaße ermitteln
    var_textlaenge = var_font.measure(var_text)
    var_texthoehe = var_font.metrics("linespace")

    # Verhältnis zum ausgabefeld ermitteln
    var_verhaeltnis = self.output_field.width / var_textlaenge

    # maximale schriftgröße ermitteln, so dass der Text in das
    # Ausgabefeld passen würde
    var_font_size_neu = int(
        self.output_field_font_size * var_verhaeltnis * 0.95)

    var_font_neu = tkf.Font(
        family=self.output_field_font, size=var_font_size_neu)

    # wenn Ergenbis Texthöhe zu hoch, Schriftgröße nochmal verringern
    if (var_font_neu.metrics("linespace") > self.output_field.height):

        var_font_size_neu = int(var_font_size_neu * var_verhaeltnis)

        var_font_neu = tkf.Font(family=self.output_field_font,
            size=var_font_size_neu)

    self.output_fieldusgabefeld.config(text = self.eingabefeld.get(),
        font=var_font_neu)
    self.text = self.eingabefeld.get()
Daraus entstand mein Skript wie folgt:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import tkinter as tk
import tkinter.font as tkf

APP_TITLE = "Text Fitter"
APP_XPOS = 100
APP_YPOS = 100
APP_WIDTH = 600
APP_HEIGHT = 310

MAX_ENTRY_CHAR = 8
OUTPUT_WIN_WIDTH = 150
OUTPUT_WIN_HEIGHT = 100


class Application(tk.Canvas):

    def __init__(self, app_win, **kwargs):
        self.app_win = app_win
        app_win.protocol("WM_DELETE_WINDOW", self.close_app)
        
        tk.Canvas.__init__(self, app_win, **kwargs)
        
    def build(self):
        self.measure_samples("yellow world")

        self.entry_frame = tk.Frame(self, bg='gray', bd=3, relief="groove",
            width=300, height=150)
        
        # Eingabefeld        
        self.entry_var = tk.StringVar()
        self.entry_var.trace("w", self.entry_trace_callback)
        
        self.entry = tk.Entry(self.entry_frame, relief = "sunken", bd=2,
            textvariable=self.entry_var, font=("Arial", 15))
        self.entry.place(x=10, y=5, width=270, height=30)
        self.entry_var.set("Name1")
        self.entry.select_range(0, len(self.entry.get()))
        self.entry.focus()
        self.entry.bind("<KeyRelease>", self.event_key_release)
        
        # Ausgabefeld
        self.output_font = tkf.Font(family="Arial", size=15)
        self.output_var = tk.StringVar()  
        self.output = tk.Label(self.entry_frame, relief="groove", bd=2, 
            font=self.output_font, bg="lightgray", textvariable=self.output_var)
        self.output.place(x=10, y=40, width=OUTPUT_WIN_WIDTH,
            height=OUTPUT_WIN_HEIGHT)
            
        self.create_window(20, 150, window=self.entry_frame, anchor="nw")

        tk.Button(self.app_win, text="close",
            command=self.app_win.destroy).pack(pady=2)
    
    def entry_trace_callback(self, *args):
        char = self.entry_var.get()[0:MAX_ENTRY_CHAR]
        self.entry_var.set(char)

    def event_key_release(self, event):
        if event.keysym == 'Escape':
            self.output_var.set("")
                        
        if event.keysym == 'Return':
            #--- Anpassen der Schriftgröße ---
            
            # Texteingabe kontrollieren
            var_text = self.entry_var.get()
            if len(var_text) == 0: return

            # Textlänge ermitteln
            var_text_length = self.output_font.measure(var_text)
            
            # Verhältnis zum Ausgabefeld ermitteln
            var_ratio = OUTPUT_WIN_WIDTH / var_text_length

            # Maximale Schriftgröße ermitteln, so dass der Text in das
            # Ausgabefeld passen würde
            old_font_size = self.output_font.cget('size')
            new_font_size = int(old_font_size * var_ratio * 0.95)   
            self.output_font.configure(size=new_font_size)
            
            # Wenn Ergenbis Texthöhe zu hoch, Schriftgröße nochmal verringern
            text_height = self.output_font.metrics("linespace")
            if text_height > OUTPUT_WIN_HEIGHT:
                var_ratio = OUTPUT_WIN_HEIGHT / text_height
                new_font_size = int(new_font_size * var_ratio)
                self.output_font.configure(size=new_font_size)
            
            # Ausgabefeld aktualisieren
            self.output.config(font=self.output_font)
            self.output_var.set(var_text)
        
    def measure_samples(self, text):
        xpos, ypos = (20,20)
        fonts = (
            tkf.Font(family='Helvetica', size=20),
            tkf.Font(family='Times', size=20))
            
        for font in fonts:
            font_fam, size, text_length, height = (
                font.cget('family'),
                font.cget('size'),
                font.measure(text),
                font.metrics("linespace"),
                )
            self.create_text(xpos, ypos, text="_{}_".format(text), font=font,
                anchor="nw")
            self.create_text(xpos, ypos + height + 5,
                text="{} {}: BxH=({},{})".format(font_fam, size, text_length,
                height), font=("Arial", 10),anchor="nw")
            ypos += height + 25
            
    def close_app(self):
        # Here do something before apps shutdown
        print("Good Bye!")
        self.app_win.destroy()

        
def main():
    app_win = tk.Tk()
    app_win.title(APP_TITLE)
    app_win.option_add("*highlightThickness", 0)
    
    app = Application(app_win, width=APP_WIDTH, height=APP_HEIGHT,
        relief="groove", bg="white", bd=2)
    app.pack()
    app.build()
    
    app_win.mainloop()
 
 
if __name__ == '__main__':
    main()
Ich habe außerdem noch ein Verständnisproblem zu Euren Programmstrukturen ...
Wenn ich es richtig verstanden habe, ist app_win eine Instanz von tk.Tk und sollte alle Module kennen.
self.app_win ist eine Referenz auf app_win.
Warum muss tk.Canvas über die Klassendefinition noch zusätzlich vererbt werden? Eigentlich müsste die Klasse doch Canvas über app_win bereits kennen?!
Darüber können dir unsere versierten und allzeit hilfsbereiten Forumsmitglieder __deets__ und Sirius3 schneller und besser Auskunft geben.
Gruss wuf :wink:
Take it easy Mates!
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@suk: noch etwas zu den Coding-Richtlinien: Deine if haben zu viele Klammern, das sind keine Funktionen, also sind Klammern um den gesamten Ausdruck unnötig. Bei `if (int(len(self.eingabefeld.get()) > maxchar)):` wandelst zu zudem noch den Wahrheitswert des >-Vergleichs in eine Zahl um!

Statt überlange Zeilen mit \ umzubrechen, ist es besser, innerhalb von Klammern umzubrechen, dann weiß Python automatisch, dass es in der nächsten Zeile weitergeht:

Code: Alles auswählen

            print("neu: (Textbreite, Textfeldbreite) (Texthöhe, Textfeldhöhe): ({}, {}) ({}, {})".format(
                var_font_neu.measure(var_text), self.a_x, var_font_neu.metrics("linespace"), self.a_y))
Zu Deinen Fragen:
suk hat geschrieben:Wenn ich es richtig verstanden habe, ist app_win eine Instanz von tk.Tk und sollte alle Module kennen.
`app_win` ist eine Instanz von tk.Tk, kennt also alle Methoden.
suk hat geschrieben:Warum muss tk.Canvas über die Klassendefinition noch zusätzlich vererbt werden? Eigentlich müsste die Klasse doch Canvas über app_win bereits kennen?!
`app` dagegen ist die eigentliche Applikation; wuf hat die Designentscheidung getroffen, dass `app` ein Canvas IST. Damit hat app alle Eigenschaften von Canvas und zusätzlich noch das, was in der Klasse definiert wurde.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

Danke für die guten Tips. Werde ich so übernehmen.

@wuf
Übrigens wirft dieser Fall bei deinem Skript in Zeile 82 die Exception:
var_verhaeltnis = self.a_x / var_textlaenge
ZeroDivisionError: division by zero
.. hatte ich auch schon bemerkt. Da war jedoch schon eingestellt.
Was bezweckst du in Zeile 71 mit dieser Textrückkopplung?
self.eingabefeld.config(text = self.eingabefeld.get())
.. war wohl schon ein bisschen spät ;) Sollte eigentlich ein Rücksetzen auf den Ursprungswert werden.
Noch weiter Tipps:
a) Versuche die PEP8 Richtlinen zu befolgen
b) Codezeilenlänge möglichts maximal auf Seitenbreite begrenzen (Seite im Hochformat).
Überlange Kodezeilen können mit einem \ gekürzt werden
c) Ein Skript möglichts ohne Überlange print-Anweisungen ins Forum platzieren
d) Im Forum möglichst die nummerierten Code Tags benutzen
..
zu a) Wo finde ich die PEP8 Richtlinien?
zu d) Ich benutze immer "Code". Mit der Nummerierung ist natürlich besser. Welche Formatierungsfunktion müsste ich denn nutzen?

@Sirius3
@suk: noch etwas zu den Coding-Richtlinien: Deine if haben zu viele Klammern, das sind keine Funktionen, also sind Klammern um den gesamten Ausdruck unnötig. Bei `if (int(len(self.eingabefeld.get()) > maxchar)):` wandelst zu zudem noch den Wahrheitswert des >-Vergleichs in eine Zahl um!
.. da ich öfters auch mehrere Bedingungen verknüpfe, finde ich es lesbarer, die Einzelbedingungen zu klammern. Bei konsequenter Umsetzung sind dann halt auch einfache Bedingungen geklammert. Kann dies zu Problemen führen oder ist es nur ein Design-Thema?
Meine Klammersetzung war in dem genannten Fall nicht so gedacht bzw. falsch. Int sollte eigentlich nur das Ergebnis von len(self.eingabefeld.get()) definitiv als Integer umwandeln. Wäre aber wahrscheinlich nicht notwendig gewesen. Kommt daher, dass ich schon mal eine Fehlermeldung hatte, dass String und Integer nicht verglichen werden konnten. Ich tue mich manchmal noch recht schwer damit einzuschätzen, welchen Typ die Rückgabewerte haben.
narpfel
User
Beiträge: 643
Registriert: Freitag 20. Oktober 2017, 16:10

@suk: PEP8 findet man hier. Das hätte übrigens auch eine kurze Google-Suche ergeben. :wink: Und die nummerierten Code-Tags verbergen sich hinter dem Dropdown-Menü „Code auswählen“.

Ad Klammerung: Für jeden beliebigen Ausdruck `x` ist `(x)` genau das selbe. Von daher macht das für Python keinen Unterschied. Es liest sich einfach nur komisch, weil die Klammern eben überflüssig sind. Das Argument, dass du das aus Konsistenz machst, weil du bei mehreren verknüpften Bedingungen auch klammerst, ist ähnlich sinnvoll wie das Argument, dass man ja bei der Addition zweier Zahlen ein Pluszeichen und links und rechts jeweils ein Objekt hat und deswegen jeden ganzzahligen Ausdruck immer mit `+ 0` abschließt, weil man bei der Addition ja auch ein Plus braucht. :wink:

Das Klammerargument und das Ergebnis von `len` in einen `int` umzuwandeln, hat ein wenig was von Cargo-Kult-Programmierung. `len` gibt immer einen `int` zurück. Wie sollte eine Sequenz auch 42,5 Werte enthalten? Oder `"foo"` viele Werte‽
suk hat geschrieben:Ich tue mich manchmal noch recht schwer damit einzuschätzen, welchen Typ die Rückgabewerte haben.
Mit der Zeit bekommt man ein Gefühl dafür. Bis dahin hilft die Dokumentation. :wink:
Benutzeravatar
Kebap
User
Beiträge: 686
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

narpfel hat geschrieben:
suk hat geschrieben:Ich tue mich manchmal noch recht schwer damit einzuschätzen, welchen Typ die Rückgabewerte haben.
Mit der Zeit bekommt man ein Gefühl dafür. Bis dahin hilft die Dokumentation. :wink:
Zum Glück muss man gar nicht schätzen, sondern Python bietet eine interaktive Konsole, wo man so leichte Fragen leicht prüfen und selbst beantworten lassen kann, hier z.B. mit dem Befehl type():

Code: Alles auswählen

>>> type(len("Test"))
<class 'int'>
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
suk
User
Beiträge: 17
Registriert: Sonntag 17. Dezember 2017, 01:18

narpfel hat geschrieben:@suk: PEP8 findet man hier. Das hätte übrigens auch eine kurze Google-Suche ergeben. :wink: Und die nummerierten Code-Tags verbergen sich hinter dem Dropdown-Menü „Code auswählen“.
Habe mir mal gestern PEP8 durchgelesen.
Da freut man sich, wenn man Fortschritte in der Syntax macht ... und dann ist noch soviel Design zu beachten. Macht ja alles sicher Sinn, ist jedoch sehr umfangreich.

Mit der Zeilenbegrenzung wird man m.E. irgendwann an Grenzen stoßen ...
Wie müsste ich denn folgenden Code umbrechen, um nach PEP8 die Zeilenlänge von 72? Zeichen noch einhalten zu können?

Code: Alles auswählen

        if var_font.measure(self.mydata.player_name[self.mydata.playerqueue[position]]) >= var_label_width_max:
            var_font_size = int(var_font_size * (var_label_width_max / var_font.measure(self.mydata.player_name[self.mydata.playerqueue[position]])))

Das Klammerargument und das Ergebnis von `len` in einen `int` umzuwandeln, hat ein wenig was von Cargo-Kult-Programmierung. `len` gibt immer einen `int` zurück. Wie sollte eine Sequenz auch 42,5 Werte enthalten? Oder `"foo"` viele Werte‽
.. Cargo-Kult nehme ich mal als Kompliment ;)
Hast aber Recht, mit meinen bisher zwei Monaten Python habe ich mir noch viel zu erarbeiten. Aber dazu hoffe ich ja im Forum Anregungen und Hinweise zu bekommen.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@suk: die 72 Zeichen ist einer der wenigen Punkte, die nicht so streng gesehen werden. Bei Deinem Code sollte man ein paar Variablen einführen:

Code: Alles auswählen

player_name = self.mydata.player_name[self.mydata.playerqueue[position]]
width = var_font.measure(player_name)
if  width >= var_label_width_max:
    var_font_size = int(var_font_size * (var_label_width_max / width))
narpfel
User
Beiträge: 643
Registriert: Freitag 20. Oktober 2017, 16:10

@suk: Die 72 Zeichen beziehen sich auf Fließtext, also Docstrings und längere Kommentare. Da ist es im Prinzip egal, wo man umbricht, also sollte man die Zeilenlänge so beschränken, dass es gut lesbar bleibt.

Bei der Zeilenlänge für Code halte ich es auch eher mit Raymond Hettinger: Es ist besser, die 79 Zeichen ein wenig zu überschreiten, als dafür Lesbarkeit zu opfern, indem man Zeilen an schlechten Stellen umbricht oder Variablennamen verkürzt. Über die alternativ erlaubten 99 Zeichen würde ich aber nicht herausgehen.

Deinen Code könnte man verbessern, indem man Werte, die mehrfach berechnet werden, an Namen bindet:

Code: Alles auswählen

        player_name = self.mydata.player_name[self.mydata.playerqueue[position]]
        player_name_width = var_font.measure(player_name)
        if player_name_width >= var_label_width_max:
            var_font_size = int(var_font_size * var_label_width_max / player_name_width)
Alternativ finde ich auch so etwas in Ordnung:

Code: Alles auswählen

        player_name_width = var_font.measure(
            self.mydata.player_name[self.mydata.playerqueue[position]]
        )
        if player_name_width >= var_label_width_max:
            var_font_size = int(var_font_size * var_label_width_max / player_name_width)
Sonstige Anmerkungen: Ein `my`-Präfix vor Namen ist überflüssig, weil es keinerlei Aussagekraft hat. `data` ist auch ein sehr generischer Name, für den man eventuell etwas besseres finden könnte. Das `var_`-Präfix sieht auch komisch aus. Wozu ist das gut?

Eventuell macht es auch Sinn, eine `Player`-Klasse (oder ein `namedtuple`) zu schreiben, weil du in `mydata.playerqueue` anscheinend Indizes in andere Listen speicherst. Es ist sinnvoller, zusammengehörige Daten auch zusammen zu speichern. `playerqueue` ist als Name für eine Liste (?) auch suboptimal, weil es den Datentypen `Queue` gibt.
Antworten