Changing the editor's color scheme

Begin with a new file named colorchooser.py in the same directory as the rest of your Python files. In this file, we will be creating another Toplevel window, which gives the user the ability to set some color variables.

Once again, we have the problem of needing to get some information from a user in a specified format. Luckily, Tkinter is on our side once again, and has provided a colorchooser module that allows us to collect color choices from the user. We again need only one function from this module, called askcolor:

import tkinter as tk
import tkinter.ttk as ttk
from tkinter.colorchooser import askcolor

We begin this file by importing this function from the colorchooser module found within Tkinter. We can now begin creating our class:

class colorChooser(tk.Toplevel):
def __init__(self, master, **kwargs):
super().__init__(**kwargs)
self.master = master

self.transient(self.master)
self.geometry('400x300')
self.title('color Scheme')

This class begins in almost exactly the same way as our FontChooser, so it should not need explaining:

self.chosen_background_color = tk.StringVar()
self.chosen_foreground_color = tk.StringVar()
self.chosen_text_background_color = tk.StringVar()
self.chosen_text_foreground_color = tk.StringVar()

Four StringVars are created. These will hold the following:

  • The background color of the TextEditor (and all secondary Toplevel windows)
  • The foreground color of the TextEditor (and all secondary Toplevel windows)
  • The background color of the TextArea widget
  • The foreground color of the TextArea widget

The four StringVars are defaulted to attributes that will be stored in the TextEditor class. We will add these later:

self.chosen_background_color.set(self.master.background)
self.chosen_foreground_color.set(self.master.foreground)
self.chosen_text_background_color.set(self.master.text_background)
self.chosen_text_foreground_color.set(self.master.text_foreground)

To lay out the window we will need a few different Frame widgets:

window_frame = tk.Frame(self, bg=self.master.background)
window_foreground_frame = tk.Frame(window_frame, bg=self.master.background)
window_background_frame = tk.Frame(window_frame, bg=self.master.background)

text_frame = tk.Frame(self, bg=self.master.background)
text_foreground_frame = tk.Frame(text_frame, bg=self.master.background)
text_background_frame = tk.Frame(text_frame, bg=self.master.background)

self.all_frames = [window_frame, window_foreground_frame,
window_background_frame, text_frame,
text_foreground_frame, text_background_frame]

We create two main Frame instances, one for the window settings and one for the text settings. Each of these then contain a Frame for the foreground settings and one for the background settings.

Each of these Frame instances has its background color set to the background attribute of the TextEditor.

Since we want to be changing the color of these frames as the user updates them, we keep a reference to all of them as an all_frames attribute. That way we can easily loop over this list and configure them to the new color.

Now that we have our layout elements in place we can start adding widgets.

For each configurable option we will need:

  • A Label to indicate what the user is changing
  • A way for the user to choose a color
  • A preview of the chosen option

To achieve this, we will be using two Label widgets for the textual information, and a Button which will utilize the askcolor function to display a color chooser to the user.

We also require a Label to indicate whether the color changes will impact the application window or the TextArea.

Let's begin by adding some Label widgets into our window:

window_label = ttk.Label(window_frame, text="Window:", anchor=tk.W, style="editor.TLabel")
foreground_label = ttk.Label(window_foreground_frame, text="Foreground:", anchor=tk.E, style="editor.TLabel")
background_label = ttk.Label(window_background_frame, text="Background:", anchor=tk.E, style="editor.TLabel")

The first group handles the window subtitle and both of the indicator labels. Each Label in this class will have a style of editor.TLabel. This will be configured by the TextEditor class once we are finished on this window:

text_label = ttk.Label(text_frame, text="Editor:", anchor=tk.W, style="editor.TLabel")

text_foreground_label = ttk.Label(text_foreground_frame, text="Foreground:", anchor=tk.E, style="editor.TLabel")

text_background_label = ttk.Label(text_background_frame, text="Background:", anchor=tk.E, style="editor.TLabel")

The second group does the same but for the text area configurations.

Now that we have our indication Label widgets in place we can get on with the actual functionality—the color chooser:

foreground_color_chooser = ttk.Button(window_foreground_frame, text="Change Foreground color", width=26,   style="editor.TButton", command=lambda sv=self.chosen_foreground_color: self.set_color(sv))


background_color_chooser = ttk.Button(window_background_frame, text="Change Background color", width=26, style="editor.TButton", command=lambda sv=self.chosen_background_color: self.set_color(sv))


text_foreground_color_chooser = ttk.Button(text_foreground_frame, text="Change Text Foreground color", width=26, style="editor.TButton", command=lambda sv=self.chosen_text_foreground_color: self.set_color(sv))


text_background_color_chooser = ttk.Button(text_background_frame, text="Change Text Background color", width=26, style="editor.TButton", command=lambda sv=self.chosen_text_background_color: self.set_color(sv))

Four Button widgets are created. Each is essentially the same, the differences are which Frame is the parent and which StringVar we want to store the color choice in. Just like our Label widgets, all Button widgets in our application will need a style named editor.TButton.

To get a feel of how the Button widgets will function let's jump to the method they are all calling: set_color:

def set_color(self, sv):
choice = askcolor()[1]
sv.set(choice)

This function takes a StringVar argument, calls the askcolor function, parses the second return value from it, and stores the result in the StringVar.

The askcolor function pops up a window with three color sliders—red, green, and blue. The user can then slide each primary color to create the color they are after. There is also an Entry in which they can enter the hex value of their color choice if they know it:

This function returns a tuple of two items. The first is another tuple of the chosen red, green, and blue amounts as floating point numbers. The second is the hex string of their chosen color. Since Tkinter works with hex strings by default, we can disregard the tuple of floats and just grab the hex string.

With this covered we can return to the __init__ method and continue setting up our widgets:

foreground_color_preview = ttk.Label(window_foreground_frame, textvar=self.chosen_foreground_color, style="editor.TLabel")

background_color_preview = ttk.Label(window_background_frame, textvar=self.chosen_background_color, style="editor.TLabel")

text_foreground_color_preview = ttk.Label(text_foreground_frame, textvar=self.chosen_text_foreground_color, style="editor.TLabel")

text_background_color_preview = ttk.Label(text_background_frame, textvar=self.chosen_text_background_color, style="editor.TLabel")

Four more Label widgets will show the user their choice of color next to each Button. Since the chosen colors are stored in each StringVar, we just need to set these Label widgets to display the StringVar's value.

Just one more widget to define now—a Save button. This will write the user's choices to a .yaml file once again, so that each time they open the editor it is in the same color scheme as when they closed it:

save_button = ttk.Button(self, text="save", command=self.save, style="editor.TButton")

That's it for the widgets! We will finish off our __init__ method with some calls to pack:

window_frame.pack(side=tk.TOP, fill=tk.X, expand=1)
window_label.pack(side=tk.TOP, fill=tk.X)

window_foreground_frame.pack(side=tk.TOP, fill=tk.X, expand=1)
window_background_frame.pack(side=tk.TOP, fill=tk.X, expand=1)

foreground_label.pack(side=tk.LEFT, padx=30, pady=10)
foreground_color_chooser.pack(side=tk.LEFT)
foreground_color_preview.pack(side=tk.LEFT, expand=1, fill=tk.X, padx=(15, 0))

background_label.pack(side=tk.LEFT, fill=tk.X, padx=(30, 27))
background_color_chooser.pack(side=tk.LEFT)
background_color_preview.pack(side=tk.LEFT, expand=1, fill=tk.X, padx=(15, 0))

Begin by packing our application settings. We first pack the Frame widgets, then our three widgets for the foreground, and finally our three widgets for the background:

text_frame.pack(side=tk.TOP, fill=tk.X, expand=1)
text_label.pack(side=tk.TOP, fill=tk.X)

text_foreground_frame.pack(side=tk.TOP, fill=tk.X, expand=1)
text_background_frame.pack(side=tk.TOP, fill=tk.X, expand=1)

text_foreground_label.pack(side=tk.LEFT, padx=30, pady=10)
text_foreground_color_chooser.pack(side=tk.LEFT)
text_foreground_color_preview.pack(side=tk.LEFT, expand=1, fill=tk.X, padx=(15, 0))

text_background_label.pack(side=tk.LEFT, fill=tk.X, padx=(30, 27))
text_background_color_chooser.pack(side=tk.LEFT)
text_background_color_preview.pack(side=tk.LEFT, expand=1, fill=tk.X, padx=(15, 0))

The same is repeated for the TextArea widgets.

Finally, just our Save button left to pack:

save_button.pack(side=tk.BOTTOM, pady=(0, 20))

That's our huge __init__ method completed! If you want to preview it, add an if __name__ == '__main__' block and give this file a run.

Otherwise, we'll finish off this class by writing our save method, which will write a .yaml file for persistent storage:

def save(self):
yaml_file_contents = f"background: '{self.chosen_background_color.get()}' "
+ f"foreground: '{self.chosen_foreground_color.get()}' "
+ f"text_background: '{self.chosen_text_background_color.get()}' "
+ f"text_foreground: '{self.chosen_text_foreground_color.get()}' "

with open("schemes/default.yaml", "w") as yaml_file:
yaml_file.write(yaml_file_contents)

The .yaml file will contain our user's four choices. We use a formatted string to inject the values of our StringVars into a basic .yaml syntax. Be sure to notice that each injected variable should be wrapped in a string, since the # character will be in our values and this constitutes a comment in .yaml.

We then open a .yaml file stored in our schemes folder and write the contents of our string into that file.

Now that we have saved the user's choices, we need to pass over to our TextEditor class and tell it to recolor the application:   

self.master.apply_color_scheme(self.chosen_foreground_color.get(),
self.chosen_background_color.get(),
self.chosen_text_foreground_color.get(),
self.chosen_text_background_color.get())

A method called apply_color_scheme is called and we pass the four StringVar's values as arguments.

To finish off our save method we also need to update the colorChooser window itself, so we need to configure the visible widgets. This is where our all_frames list comes in handy:

for frame in self.all_frames:
frame.configure(bg=self.chosen_background_color.get())

self.configure(bg=self.chosen_background_color.get())

We loop through all of our Frame widgets and configure their background to the value of the relevant StringVar. The window itself is also configured to use the new background color.

All of the Label and Button widgets are handled by the TextEditor class (using the style argument mentioned earlier). Let's write this code now:

class TextEditor(tk.Tk):
...
def __init__(self):
...
self.background = 'lightgrey'
self.foreground = 'black'
self.all_menus = [self.menu, self.right_click_menu]
...
def generate_sub_menus(self, sub_menu_items):
...
self.all_menus.append(sub_menu)
def apply_color_scheme(self, foreground, background,
text_foreground, text_background):
self.background = background
self.foreground = foreground
self.text_area.configure(fg=text_foreground, bg=text_background)

for menu in self.all_menus:
menu.configure(bg=self.background, fg=self.foreground)
self.configure_ttk_elements()

Inside apply_color_scheme the user's chosen values from the colorChooser window are now saved as attributes of the TextEditor class so that they can be sent around to any other windows that may need them.

The TextArea widget is configured to use the foreground and background, which were passed to this function. This is all we need to do for this widget.

In order to change all of our menus we will need to loop through them. This requires a list of them, much like we did with the Frame widgets back in our colorChooser class. This list is defined in our __init__ method after creating the menu bar and right-click menu, then appended to at the end of each loop in our generate_sub_menus method.

Once all of the configure calls are done we still need to update our ttk widgets. The configure_ttk_elements method lets us do this:

def configure_ttk_elements(self):
style = ttk.Style()
style.configure('editor.TLabel', foreground=self.foreground, background=self.background)
style.configure('editor.TButton', foreground=self.foreground, background=self.background)

A Style object is created, which is how the application will handle ttk element styling. The Style then configures the two style strings, which we saw in our colorChoosereditor.TLabel and editor.TButton. These have their foreground set to the value in our foreground attribute, and their background set to the value of our background attribute.

To ensure the new colors are applied when the application loads, make sure you change every hard-coded bg='lightgrey' to bg=self.background and every fg='black' to fg=self.foreground. These should be at the creation of self.menu, the creation of each submenu in generate_sub_menus, and the creation of our right-click context menu.

You can now go ahead and run the texteditor.py file to see the colorChooser in action. Go ahead and change some of the colors and watch how the editor reacts:

There are now some tweaks we need to make to the other classes to make sure they use the chosen color scheme.

In the FontChooser's __init__ method, we need to set its background color to that of the application. The Save button also needs to take the editor.TButton style:

class FontChooser(tk.Toplevel):
def __init__(self, master, **kwargs):
...
self.configure(bg=self.master.background)
...
self.save_button = ttk.Button(self, text="Save",
style="editor.TButton", command=self.save)

Similarly, our FindWindow now needs to be updated. The three Button widgets should get our editor.TButton style, the Label widgets need to become ttk Label widgets with our editor.TLabel style, and all Frame and window colors need to reflect the application's.

Since the master widget of our FindWindow is actually the TextArea, we need to use self.master.master to refer to our TextEditor:

class FindWindow(tk.Toplevel):
def __init__(self, master, **kwargs):
...
self.configure(bg=self.master.master.background)
...
top_frame = tk.Frame(self, bg=self.master.master.background)
middle_frame = tk.Frame(self, bg=self.master.master.background)
bottom_frame = tk.Frame(self, bg=self.master.master.background)
...
find_entry_label = ttk.Label(top_frame, text="Find: ",
style="editor.TLabel")
...
replace_entry_label = ttk.Label(middle_frame, text="Replace: ",
style="editor.TLabel")
...
self.find_button = ttk.Button(bottom_frame, text="Find",
command=self.on_find, style="editor.TButton")
self.replace_button = ttk.Button(bottom_frame, text="Replace",
command=self.on_replace, style="editor.TButton")
self.cancel_button = ttk.Button(bottom_frame, text="Cancel",
command=self.on_cancel, style="editor.TButton")

And with that we are finished with our color changing code! All we need to do is wrap it in a function, which will add it to the Tools menu:

def change_color_scheme(self):
colorChooser(self)

def tools_change_color_scheme(self, event=None):
"""
Ctrl+G
"""
self.change_color_scheme()

This also marks the end of the Tools menu. That leaves us with just one more submenu to add something to—the Help menu.

In the Help menu we can just add some information about our application. If you would like, you can add some detail and images here. For the sake of this example I will just keep it simple:

import tkinter.messagebox as msg

def show_about_page(self):
msg.showinfo("About", "My text editor, version 3, written in
Python3.6 using tkinter!")

def help_about(self, event=None):
"""
Ctrl+H
"""
self.show_about_page()

A simple showinfo box displays some information to the user about the version number and technologies of the application.

All of the functionality of our editor is now finished.

The last thing to do to fully complete the application is to bind all of the keys that are displayed in our submenus, since unfortunately they do not bind automatically:

def bind_events(self):
...
self.bind('<Control-n>', self.file_new)
self.bind('<Control-o>', self.file_open)
self.bind('<Control-s>', self.file_save)

self.bind('<Control-h>', self.help_about)

self.bind('<Control-m>', self.tools_change_syntax_highlighting)
self.bind('<Control-g>', self.tools_change_color_scheme)
self.bind('<Control-l>', self.tools_change_font

With that our text editor application is complete! We have now created a functioning text editor which boasts:

  • A working menu bar and right-click context menu
  • The ability to open and save files
  • Keyboard shortcuts and context menu items to cut, copy, and paste text
  • The ability to choose the font and font size
  • Syntax highlighting for any file type we can write a .yaml file for
  • A customizable color scheme for both the application and the text area
  • A functioning find/replace window

This is where we shall leave this project. If you feel that there is some functionality that another text editor has which you like, feel free to try and implement it into this project as an exercise.

The third and final application we will move on to will be an online instant messaging program. We may even see our old friend the Text widget there!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset