Posts: 21
Threads: 5
Joined: Sep 2024
Sep-30-2024, 03:27 AM
Hello Python Community
I'm new to Python. I'm using Visual Studio Code in Windows10, latest version of Python. I'm using Tkinter GUI.
I'm making a text editor to practice and learn, currently my goal is to make a Font Dialog box, which when I click the OK Button should set the font (selected from the LIstbox widget) to the Text widget and disappear. What occurs when I try is that the Font changes but the Dialog box doesn't disappear. There is no error message, so I suppose it must be something conceptual. The Font size changes but the Font family doesn't.
I give you here a minimal code example that reproduce the error, or I should say "the non working as expected".
import tkinter as tk
from tkinter import *
from tkinter import font
class ModalForm(object):
def __init__(self, root, title, text):
self.option = 'Cancel'
self.myfont = None
self.modal = tk.Toplevel(root)
self.modal.title(title)
self.modal.geometry("350x300+100+100")
self.modal.resizable(False, False)
self.list = tk.Listbox(self.modal, selectmode=SINGLE, width=60)
for f in font.families():
self.list.insert('end', f)
self.list.place(x=10, y=10)
btnOk = tk.Button(self.modal, text="OK", command=self.chooseFont)
btnCancel = tk.Button(self.modal, text="Cancel", command=self.modal.destroy)
btnOk.place(x=200, y=270)
btnCancel.place(x=280, y=270)
def chooseFont(self):
self.option = 'OK'
self.myfont = self.list.curselection()
if self.myfont:
self.myfont = self.myfont[0]
global editor
def show_font_dialog():
global editor
modal = ModalForm(app, "Font Dialog", "")
app.wait_window(modal.modal)
if modal.option == 'OK':
editor.configure(font = (modal.myfont, 30, 'bold'))
modal.modal.destroy()
global editor
app = tk.Tk()
app.title("Tkinter Test modal dialog")
app.geometry("500x400+100+100")
btn = tk.Button(text="Show modal form", command=show_font_dialog)
btn.place(x=340, y=340)
editor = tk.Text(app, undo=True, maxundo=-1)
editor.place(x=0, y=0, width=470, height=250)
app.mainloop()I will appreciate any help you can give me.
Thanks in advance.
Pablo
Posts: 1,144
Threads: 114
Joined: Sep 2019
Sep-30-2024, 10:25 AM
(This post was last modified: Sep-30-2024, 05:05 PM by menator01.)
Here is one way to do it. Be aware that this changes the font for all of the text widget.
import tkinter as tk
from tkinter import ttk, font
class TopWindow:
''' Class creates a toplevel window '''
def __init__(self):
pass
def font_list(self):
''' Method displays list of fonts. Using treeview to format fonts '''
self.window = tk.Toplevel(None)
self.window.geometry('300x400+300+300')
self.window.columnconfigure(0, weight=1)
self.window.rowconfigure(0, weight=1)
self.window['padx'] = 10
self.window['pady'] = 10
style = ttk.Style()
style.configure('Treeview', rowheight=30)
self.box =ttk.Treeview(self.window, show='tree', style='Treeview', selectmode='browse')
self.box.grid(column=0, row=0, sticky='news')
for item in sorted(font.families()):
self.box.tag_configure(item, font=(item, 12, 'normal'))
self.box.insert('', 'end', text=item, values=(item.strip(),), tags=(item,))
# Create the scrollbar
scrollbar = tk.Scrollbar(self.window, orient='vertical')
scrollbar.grid(column=1, row=0, sticky='ns', padx=2, pady=5)
scrollbar.configure(command=self.box.yview)
self.box.configure(yscrollcommand=scrollbar.set)
btnframe = tk.LabelFrame(self.window, text='Options')
btnframe.grid(column=0, row=1, sticky='news')
# Create the button
self.button = tk.Button(btnframe, text='Choose Font')
self.button.grid(column=0, row=0, padx=5, pady=5)
cancel_button = tk.Button(btnframe, text='Cancel', command=self.window.destroy)
cancel_button.grid(column=1, row=0, padx=5, pady=5)
cancel_button.configure(bg='tomato', activebackground='orangered', cursor='hand2')
class Window:
''' Class for main window '''
def __init__(self, parent):
self.label = tk.Label(parent)
self.label.grid(column=0, row=0, sticky='new', padx=5, pady=5)
self.label.configure(
text = 'Font: Default',
bg = '#999999',
highlightbackground = '#555555',
highlightcolor = '#555555',
highlightthickness = 1,
font = (None, 14, 'normal')
)
self.textbox = tk.Text(parent)
self.textbox.grid(column=0, row=1, sticky='news', padx=5, pady=5)
self.textbox.configure(font=(None, 18, 'normal'))
self.button = tk.Button(parent, text='Choose Font')
self.button.grid(column=0, row=2, padx=5, pady=5)
class Controller:
''' Controller handles communication between classes '''
def __init__(self, topwindow, window):
self.topwindow = topwindow
self.window = window
# Configure main window button
self.window.button.configure(command=self.getwindow)
def getwindow(self):
# Call the popup window to list fonts
self.topwindow.font_list()
# Configure the button
self.topwindow.button.configure(command=self.getfont)
self.topwindow.window.bind('<Double-1>', lambda event: self.getfont())
def getfont(self, event=None):
# Get selected font and close toplevel window
item = self.topwindow.box.focus()
afont = self.topwindow.box.item(item).get('text')
self.window.label.configure(font=(afont, 14, 'normal'), text=f'Font: {afont}')
self.window.textbox.configure(font=(afont.lower(), 14, 'normal'))
self.topwindow.window.destroy()
if __name__ == '__main__':
root = tk.Tk()
controller = Controller(TopWindow(), Window(root))
root.mainloop()
TheTiger and Gribouillis like this post
Posts: 4,904
Threads: 79
Joined: Jan 2018
@ menator01 Excellent example. I suggest replacing line 20 with
for item in sorted(font.families()): Also how would you add a choice of the size of the font in the applet?
menator01 and TheTiger like this post
« We can solve any problem by introducing an extra level of indirection »
Posts: 6,981
Threads: 22
Joined: Feb 2020
Sep-30-2024, 09:57 PM
(This post was last modified: Sep-30-2024, 09:57 PM by deanhystad.)
If you want a modal dialog, the dialog should have it's own mainloop.
import tkinter as tk
from tkinter import font, ttk
class FontDialog(tk.Toplevel):
def __init__(self, parent=None, oldfont=None):
super().__init__(parent)
self.family = tk.Listbox(self, selectmode=tk.SINGLE, exportselection=False)
width = 1
family = oldfont[0] if oldfont else ""
selection = 0
for index, f in enumerate(font.families()):
width = max(width, len(f))
self.family.insert("end", f)
if family == f:
selection = index
self.family.pack(side=tk.TOP, padx=5, pady=5)
self.family.configure(width=width)
self.family.selection_set(selection)
frame = ttk.Frame(self)
frame.pack(side=tk.TOP, padx=5, pady=(0, 5))
ttk.Label(frame, text="Font Size").pack(side=tk.LEFT)
sizes = [8, 10, 12, 16, 24, 32]
size = oldfont[1] if oldfont else None
self.sizes = tk.IntVar(self, size if size in sizes else sizes[0])
ttk.Combobox(frame, textvariable=self.sizes, values=sizes, width=3).pack(side=tk.LEFT)
frame = ttk.Frame(self)
frame.pack(side=tk.TOP, expand=True, fill=tk.X, padx=5, pady=(0, 5))
ttk.Button(frame, text="OK", command=self.ok, width=1).pack(side=tk.LEFT, expand=True, fill=tk.X)
ttk.Button(frame, text="Cancel", command=self.quit, width=1).pack(side=tk.LEFT, expand=True, fill=tk.X)
self.value = None
self.mainloop()
def ok(self):
family = self.family.get(self.family.curselection())
size = self.sizes.get()
print(size, family)
if size and family:
self.value = (family, size)
self.quit()
def get_font(parent, font=None):
"""Convenience function for using font dialog. Would be packaged in same module as dialog."""
dialog = FontDialog(parent, font)
font = dialog.value
dialog.destroy()
return font
class MainWindow(tk.Tk):
"""Window that uses the dialog."""
def __init__(self):
super().__init__()
self.font = None
self.label = ttk.Label(self, text="Some font")
self.label.pack(padx=50, pady=10)
ttk.Button(self, text="Select Font", command=self.change_font).pack(padx=50, pady=10)
def change_font(self):
font = get_font(self, self.font)
if font:
self.font = font
self.label.configure(text=font[0], font=font)
MainWindow().mainloop()FontDialog and the get_font() function would be packaged together if you decided to put FontDialog in a module. Users would only import the get_font() function.
Posts: 21
Threads: 5
Joined: Sep 2024
Hello guys
I continued extending muy real app, based on your code examples, but now it doesn't even run. The issue, must be, that I want to make the Font Dialog box appear when I click a menu item from a menu bar.
I give you muy current code.
import tkinter as tk
from tkinter import *
from tkinter import ttk
from tkinter import font
from tkinter import messagebox
from tkinter.filedialog import askopenfilename, asksaveasfilename
global data, current_file
current_file = None
class FontDialog:
def __init__(self):
pass
def font_list(self):
self.dialog = tk.Toplevel(None)
self.dialog.geometry("300x400+300+300")
self.dialog.columnconfigure(0, weight=1)
self.dialog.rowconfigure(0, weight=1)
self.dialog['padx'] = 10
self.dialog['pady'] = 10
self.box = ttk.Treeview(self.dialog, show='tree', selectmode='browse')
self.box.grid(row=0, column=0, sticky='news')
for item in sorted(font.families()):
self.box.tag_configure(item, font=(item, 12, 'normal'))
self.box.insert('', 'end', text=item, values=(item,), tags=(item,))
scrollbar = tk.Scrollbar(self.dialog, orient='vertical')
scrollbar.grid(column=1, row=0, sticky='ns', padx=2, pady=5)
scrollbar.configure(command=self.box.yview)
self.box.configure(yscrollcommand=scrollbar.set)
btnframe = tk.LabelFrame(self.dialog, text='Options')
btnframe.grid(column=0, row=1, sticky='news')
self.button = tk.Button(btnframe, text="Choose Font")
self.button.grid(column=0, row=0, padx=5, pady=5)
cancel_button = tk.Button(btnframe, text="Cancel", command=self.dialog.destroy)
cancel_button.grid(column=1, row=0, padx=5, pady=5)
cancel_button.configure(bg='tomato', activebackground='orangered', cursor='hand2')
self.dialog.mainloop()
def getfont(self, parent):
self.font_list()
# Get selected font and close toplevel window
item = self.dialog.box.focus()
afont = self.dialog.box.item(item).get('text')
parent.editor.configure(font=(afont.lower(), 14, 'normal'))
self.window.textbox.configure(font=(afont.lower(), 14, 'normal'))
self.window.destroy()
class Editor:
global editor
def open(self):
filename = askopenfilename(parent=self.app, initialdir='C:\\Users\\PABLO\\Desktop')
file = open(filename, 'r')
editor.delete("1.0", "end")
editor.insert("1.0", file.read())
file.close()
def save(self):
if current_file == None or current_file == "":
self.save_file_as()
else:
file = open(current_file, 'w')
file.write(editor.get())
file.close()
def new_file(self):
global current_file
if editor.edit_modified() == True:
modif = messagebox.askquestion("Editor", "Los cambios en el archivo actual se perderán. ¿Guardar ahora?")
if modif == 'yes':
self.save()
editor.delete("1.0", "end")
def open_file(self):
if editor.edit_modified() == True:
modif = messagebox.askquestion("Editor", "Los cambios en el archivo actual se perderán. ¿Guardar ahora?")
if modif == 'yes':
self.save()
self.open()
else:
self.open()
def save_file(self):
global current_file
self.save()
def save_file_as(self):
filename = asksaveasfilename(parent=self.new_fileapp, initialdir='C:\\Users\\PABLO\\Desktop')
file = open(filename, "w")
file.write(editor.get())
file.close()
def print(self):
a = 10
def close_app(self):
salir = messagebox.askquestion("Editor", "¿Salir de la aplicación?")
if (salir == 'yes'):
self.app.quit()
def copy_select(self):
global data
if editor.selection_get():
data = editor.selection_get()
def cut_select(self):
global data
if editor.selection_get():
data = editor.selection_get()
editor.delete('sel.first', 'sel.last')
def paste_select(self):
global data
editor.insert(editor.index(tk.CURRENT), data)
def select_all(self):
editor.tag_add('sel', '1.0', 'end')
editor.tag_config('sel', background='blue', foreground='white')
def delete_select(self):
if (editor.selection_get()):
editor.delete('sel.first', 'sel.last')
def word_wrap(self):
editor.config(wrap=tk.WORD)
def font_select(self):
FontDialog.getfont()
def colors(self):
a=10
def about(self):
messagebox.showinfo("Acerca de", "Esta aplicación fue creada con Python y Tkinter.")
def __init__(self):
self.create_window(self)
def create_window(self):
app = tk.Tk()
app.title("Editor de archivos de texto")
app.geometry("600x400+100+100")
app.minsize(400, 250)
app.resizable(width=True, height=True)
menubar = tk.Menu(app)
app.config(menu=menubar)
app.config(bg="#80C080")
editor = tk.Text(app, undo=True, maxundo=-1)
editor.pack(expand=True, fill='both')
editor.edit_modified(False)
file_menu = tk.Menu(menubar, tearoff=0)
edit_menu = tk.Menu(menubar, tearoff=0)
format_menu = tk.Menu(menubar, tearoff=0)
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Archivo", menu=self.file_menu)
menubar.add_cascade(label="Editar", menu=self.edit_menu)
menubar.add_cascade(label="Formato", menu=self.format_menu)
menubar.add_cascade(label="Ayuda", menu=self.help_menu)
file_menu.add_command(label="Nuevo", command=self.new_file)
file_menu.add_command(label="Abrir ...", command=self.open_file)
file_menu.add_command(label="Guardar", command=self.save_file)
file_menu.add_command(label="Guardar como ...", command=self.save_file_as)
file_menu.add_separator()
file_menu.add_command(label="Imprimir ...", command=self.print)
file_menu.add_separator()
file_menu.add_command(label="Salir", command=self.close_app)
edit_menu.add_command(label="Copiar", command=self.copy_select, accelerator="Ctrl+C")
edit_menu.add_command(label="Cortar", command=self.cut_select, accelerator="Ctrl+X")
edit_menu.add_command(label="Pegar", command=self.paste_select, accelerator="Ctrl+V")
edit_menu.add_command(label="Borrar", command=self.delete_select)
edit_menu.add_separator()
edit_menu.add_command(label="Seleccionar todo", command=self.select_all)
edit_menu.add_separator()
edit_menu.add_command(label="Deshacer", command=editor.edit_undo, accelerator="Ctrl+Z")
edit_menu.add_command(label="Rehacer", command=editor.edit_redo, accelerator="Ctrl+Y")
format_menu.add_command(label="Ajuste de linea", command=self.word_wrap)
format_menu.add_separator()
format_menu.add(label="Font ...", command=self.font_select)
format_menu.add_command(label="Colores ...", command=self.colors)
help_menu.add_command(label="Ayuda")
help_menu.add_separator()
help_menu.add_command(label="Acerca de ...", command=self.about)
app.mainloop() Again, thank you very much for your kind help
Regards
Pablo
Posts: 1,144
Threads: 114
Joined: Sep 2019
Quote:I'm new to Python.
Quoting from your first post, in my opinion, if you're just starting you should learn the basics first.
The last code you posted seems to have many errors.
You should start with writing simple functions and get a basic understanding how they work.
Then maybe learn about classes and how to create methods for that class.
You are using global a lot. It's not really a good idea in my opinion. Pass arguments to the function instead.
Learn tkinter basics before tackling big projects like editors.
Just some things to think about.
Posts: 21
Threads: 5
Joined: Sep 2024
(Oct-01-2024, 05:14 AM)menator01 Wrote: Quote:I'm new to Python.
Quoting from your first post, in my opinion, if you're just starting you should learn the basics first.
The last code you posted seems to have many errors.
You should start with writing simple functions and get a basic understanding how they work.
Then maybe learn about classes and how to create methods for that class.
You are using global a lot. It's not really a good idea in my opinion. Pass arguments to the function instead.
Learn tkinter basics before tackling big projects like editors.
Just some things to think about.
Hello, Python Community
I replied this thread a few days ago and it was deleted, I said there, that I wasn't actually so new to Python (I already practiced console and tkinter several days), much less to programming, that I was a Senior .Net Developer, and I asked for help again, saying that I learn quickly (I'm a programmer), but nobody replied any more since that.
Now, I managed to make work the modal FontDialog as I wanted (taking @ deanhystad code example).
Mi new issue now (it is easier to solve than before one, I suppose) is that I have a Menu(Format) which has a Menu item (command) "Word wrap" ("Ajuste de linea"), what I want is that when I click that Menu command item its label switches between "[ ] Ajuste de linea" and "[v] Ajuste de linea", unchecking and checking the word wrap Text widget option. I searched the web and what I found doesn't work.
I give you a minimal example of my code that reproduces that error:
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("500x300+100+100")
self.title("Editor de textos Python")
menubar = tk.Menu(self)
self.config(menu=menubar)
format_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Format", menu=format_menu)
format_menu.add_command(label="[] Ajuste de linea", command=lambda: self.word_wrap(self, format_menu))
self.edit = tk.Text(self)
self.edit.pack(expand=True, fill='both')
self.edit.configure(wrap='none')
def clicked(menu, newlabel):
menu.entryconfigure(1, label=newlabel)
def getmenulabel(menu):
label = menu.entrycget(0, "label")
return label
def word_wrap(self, menu):
if self.getmenulabel(menu) == "[ ] Ajuste de linea":
self.clicked(menu, "[v] Ajuste de linea")
self.edit.configure(wrap='word')
else:
self.clicked(menu, "[ ] Ajuste de linea")
self.edit.configure(wrap='none')
MainWindow().mainloop()
I hope now you can help me again, and please, trust me, I have almost 30 years as a developer, I will understand your advice (and code). I will appreciate your help very much. Thank you.
Pablo
Posts: 6,981
Threads: 22
Joined: Feb 2020
Oct-11-2024, 06:22 PM
(This post was last modified: Oct-11-2024, 06:22 PM by deanhystad.)
Fixed a few errors in your code:
import tkinter as tk
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("500x300+100+100")
self.title("Editor de textos Python")
menubar = tk.Menu(self)
self.config(menu=menubar)
format_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Format", menu=format_menu)
format_menu.add_command(label="[] Ajuste de linea", command=lambda: self.word_wrap(format_menu))
self.edit = tk.Text(self)
self.edit.pack(expand=True, fill="both")
self.edit.configure(wrap="none")
@staticmethod
def clicked(menu, newlabel):
menu.entryconfigure(1, label=newlabel)
@staticmethod
def getmenulabel(menu):
label = menu.entrycget(0, "label")
return label
def word_wrap(self, menu):
if self.getmenulabel(menu) == "[ ] Ajuste de linea":
self.clicked(menu, "[v] Ajuste de linea")
self.edit.configure(wrap="word")
else:
self.clicked(menu, "[ ] Ajuste de linea")
self.edit.configure(wrap="none")
MainWindow().mainloop()All methods in a class are assumed to be instance methods and their first argument is a reference to the instance. The convention is to use "self" as the name for this first argument, but the reference to the instance is passed regardless of the variable name. The clicked() and getmenulabel() methods don't reference any instance or class variables, so they are really just functions declared in the class. To tell python that you want these to be functions you need to use the @staticmethod decorator.
Instead of changing the label you should use a checkbutton.
import tkinter as tk
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("500x300+100+100")
self.title("Editor de textos Python")
menubar = tk.Menu(self)
self.config(menu=menubar)
format_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Format", menu=format_menu)
self._word_wrap = tk.BooleanVar(self, False)
format_menu.add_checkbutton(label="Ajuste de linea", variable=self._word_wrap, command=self._toggle_word_wrap)
self.edit = tk.Text(self)
self.edit.pack(expand=True, fill="both")
self.edit.configure(wrap="none")
def _toggle_word_wrap(self, *args):
"""Called when word wrap menu toggles word wrap setting.."""
self.word_wrap = self._word_wrap.get()
@property
def word_wrap(self) -> bool:
"""Return if word wrap is enabled (True)."""
return self._word_wrap.get()
@word_wrap.setter
def word_wrap(self, is_set: bool):
"""Set word wrap mode True (on) or False (off)."""
self._word_wrap.set(is_set)
self.edit.configure(wrap="word" if is_set else "none")
self.edit.insert(tk.END, f"Setting wordwrap to {is_set}. ")
MainWindow().mainloop()
Posts: 21
Threads: 5
Joined: Sep 2024
Hi,
The code you gave me works perfect.
Thank you.
Just one doubt ... you mean static methods are those which don't reference the instance with the first parameter 'self' in their argument list?
Thanks.
You are very nice.
Pablo
Posts: 6,981
Threads: 22
Joined: Feb 2020
Oct-14-2024, 05:22 PM
(This post was last modified: Oct-14-2024, 05:22 PM by deanhystad.)
clicked() and getmenulabel() are functions. They don't need MainWindow to run, all they need is a menu You could write your code like this:
def clicked(menu, newlabel):
menu.entryconfigure(1, label=newlabel)
def getmenulabel(menu):
label = menu.entrycget(0, "label")
return label
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("500x300+100+100")
self.title("Editor de textos Python")
menubar = tk.Menu(self)
self.config(menu=menubar)
format_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Format", menu=format_menu)
format_menu.add_command(label="[] Ajuste de linea", command=lambda: self.word_wrap(self, format_menu))
self.edit = tk.Text(self)
self.edit.pack(expand=True, fill='both')
self.edit.configure(wrap='none')
def word_wrap(self, menu):
if getmenulabel(menu) == "[ ] Ajuste de linea":
clicked(menu, "[v] Ajuste de linea")
self.edit.configure(wrap='word')
else:
clicked(menu, "[ ] Ajuste de linea")
self.edit.configure(wrap='none')
MainWindow().mainloop()For packaging reasons you might want to make these functions look like they are members of MainWindow, but because they don't take an instance of MainWindow as an argument you need to modify the code. You could add a "self" argument and not use it.
def clicked(self, menu, newlabel):
menu.entryconfigure(1, label=newlabel)
def getmenulabel(self, menu):
label = menu.entrycget(0, "label")
return labelBut passing unused arguments is poor programming and most IDE's will complain about unreferenced arguments. Instead of passing an unused "self", use the @staticmethod decorator to tell python that the function does not accept an instance reference.
@staticmethod
def clicked(menu, newlabel):
menu.entryconfigure(1, label=newlabel)
@staticmethod
def getmenulabel(menu):
label = menu.entrycget(0, "label")
return label
|