I decided to take a look at the Tk GUI framework for Python and put together a simple mockup GUI to test various things.
I'd worked with Tk before in Perl (see my Perl CyanChat Client) and if you look at some of the Linux screenshots on that page, Tk looks ugly as hell in Perl.
Python's implementation of Tk (which they call Tkinter
) is more modern than Perl's, and there's other neat helper modules like ttk
which provides the Tk "Tile" theming engine which makes the standard Tk widgets look more modern, and takes a CSS-style approach to theming your widgets: instead of manually specifying things like background and foreground colors in each widget you program, you keep all that stuff in one central place and refer to it by name from the widgets.
For my Python mockup test app, I put together a rough copy of my Perl CyanChat Client GUI.
At first I was trying to use the ttk
/Tile versions of the widgets (such as Button
, Entry
, etc.), but I ran into a rather annoying roadblock: in PCCC, my text Entry widgets have black background colors, and the insertion cursor (the little flashing I-beam in a text box) is also black by default. So when clicking in the text box, you wouldn't be able to see the insertion cursor.
In the standard Tkinter Entry
widget, you can use the insertbackground
option to change the color of the insertion cursor. But in ttk/Tile? There is no insertbackground option. Source - it's just not supported in ttk/Tile and their theming engine.
So I decided to not use ttk and just use the standard Tk widgets. I liked ttk's centralized styling system though, so I made a central class of dictionaries full of configuration attributes that I could easily reference when setting up my widgets. So, I eventually got my GUI put together and it looked nice enough I guess...
Except for those ugly scrollbars. The "1980s 3D" look to the scrollbar and those ugly triangle arrow widgets are from the Motif GUI which Tk was originally modeled to look like. It's ancient and it's ugly. This was also one of the main reasons why my Perl CyanChat Client looks so horrible under Linux, because this is Tk and Tk is ancient.
The Tile theming engine is supposed to fix this, but I wasn't using Tile in my code because of the aforementioned text insertion cursor problem. The best I could do with the standard Tk scrollbar is color it to make it look kind of "cool" at least, so I made it all black and grey to fit the theme of the rest of my GUI.
But then I figured out I can mix and match the widgets. I could import the Scrollbar from ttk while importing all the other widgets from Tkinter. The result?
That's better.
I probably won't create a full CyanChat client in Python because I really don't care about CyanChat much anymore, so this was mostly just me messing around with Tk and seeing how practical it is for certain use cases. But here's the source code anyway.
There's a few interesting things in the code, like I created my own "Scrolled" class for wrapping a widget in a scrollbar (works with Text and Listbox), so it's kinda like Python's ScrolledText
module, but it's really more like Perl's Tk::Scrolled
module in that it can wrap arbitrary widgets, not just Text.
Also, Tkinter's Text widget can't be made read-only. You can make a text box disabled, but that also prevents programmatic insertions/deletions as well. So I made a little function for inserting text that would first re-enable it, then insert text, then disable it again.
#!/usr/bin/env python
"""My test script for Python/Tk experimentation."""
import Tkinter as tk
from Tkinter import Tk, StringVar, Frame, Label, Text, Entry, Button, Listbox, END
from ttk import Scrollbar
class ChatClient(object):
def __init__(self):
# Styles
self.style = MainWindowStyles()
self.setup()
def setup(self):
self.mw = Tk()
self.mw.title("Python CyanChat Client")
resize_and_center(self.mw, 640, 480)
# Variables
self.nickname = StringVar(self.mw, "Kirsle")
self.message = StringVar(self.mw, "--disabled--")
# Top Frame (name entry box, buttons, conn status)
self.login_frame = Frame(self.mw, **self.style.Frame)
self.lower_frame = Frame(self.mw, **self.style.Frame)
self.login_frame.pack(side="top", fill="x")
self.lower_frame.pack(side="top", fill="both", expand=1)
# The lower left (message entry, chat history) and lower right
# (who lists)
self.left_frame = Frame(self.lower_frame, **self.style.Frame)
self.right_frame = Frame(self.lower_frame, **self.style.Frame)
self.right_frame.pack(side="right", fill="y")
self.left_frame.pack(side="right", fill="both", expand=1)
# The message entry & chat history frames
self.message_frame = Frame(self.left_frame, **self.style.Frame)
self.dialogue_frame = Frame(self.left_frame, **self.style.Frame)
self.message_frame.pack(side="top", fill="x")
self.dialogue_frame.pack(side="top", fill="both", expand=1)
###
# Top Frame Widgets
###
self.name_label = Label(self.login_frame,
text="Name:",
**self.style.Label
)
self.name_entry = Entry(self.login_frame,
textvariable=self.nickname,
width=20,
**self.style.DarkEntry
)
self.enter_exit_button = Button(self.login_frame,
text="Enter chat",
**self.style.Button
)
self.status_label = Label(self.login_frame,
text="Connected to CyanChat",
**self.style.ConnectedLabel
)
self.name_label.pack(side="left", padx=5, pady=5)
self.name_entry.pack(side="left", pady=5)
self.enter_exit_button.pack(side="left", padx=5, pady=5)
self.status_label.pack(side="left")
###
# Message Frame Widgets
###
self.message_entry = Entry(self.message_frame,
textvariable=self.message,
state="disabled",
**self.style.Entry
)
self.message_entry.pack(
side="top",
fill="x",
padx=10,
pady=10,
expand=1,
)
###
# Who Frame Widgets
###
self.who_label = Label(self.right_frame,
text="Who is online:",
anchor="w",
**self.style.Label
)
self.who_label.pack(side="top", fill="x")
self.who_list = Scrolled(self.right_frame, Listbox,
attributes=self.style.Listbox,
scrollbar=self.style.Scrollbar,
)
self.who_list.pack(side="top", fill="both", expand=1)
for i in range(200):
self.who_list.widget.insert(END, "Anonymous{}".format(i))
###
# Dialogue Frame Widgets
###
self.dialogue_text = Scrolled(self.dialogue_frame, Text,
attributes=self.style.Dialogue,
scrollbar=self.style.Scrollbar,
)
self.chat_styles(self.dialogue_text.widget)
self.dialogue_text.pack(side="top", fill="both", padx=10, pady=0, expand=1)
# Dummy junk
messages = [
[["[Kirsle]", "user"], [" Hello room!"]],
[["\\\\\\\\\\", "server"], ["[Kirsle]", "user"], [" <links in from comcast.net Age>"], ["/////", "server"]],
[["[ChatServer] ", "server"], ["Welcome to the Cyan Chat room."]],
[["[ChatServer] ", "server"], ["There are only a few rules:"]],
[["[ChatServer] ", "server"], [" Be respectful and sensitive to others"]],
[["[ChatServer] ", "server"], [" And HAVE FUN!"]],
[["[ChatServer] ", "server"], [""]],
[["[ChatServer] ", "server"], ["Termination of use can happen without warning!"]],
[["[ChatServer] ", "server"], [""]],
[["[ChatServer] ", "server"], ["Server commands now available, type !\\? at the beginning of a line."]],
[["[ChatServer] ", "server"], ["CyanChat Server version 2.12d"]],
]
for i in range(80):
messages.append([["[ChatClient]", "client"], [" Connecting..."]])
messages.reverse()
for line in messages:
self.insert_readonly(self.dialogue_text, 0.0, "\n")
line.reverse()
for part in line:
self.insert_readonly(self.dialogue_text, 0.0, *part)
#self.insert_readonly(self.dialogue_text, END, "[Admin]", "admin")
def chat_styles(self, widget):
"""Configure chat text styles."""
# User colors
widget.tag_configure("user", foreground="#FFFFFF")
widget.tag_configure("guest", foreground="#FF9900")
widget.tag_configure("admin", foreground="#00FFFF")
widget.tag_configure("server", foreground="#00FF00")
widget.tag_configure("client", foreground="#FF0000")
def insert_readonly(self, widget, *args):
"""Insert text into a readonly (disabled) widget."""
widget.widget.configure(state="normal")
widget.widget.insert(*args)
widget.widget.configure(state="disabled")
def start(self):
self.mw.mainloop()
class MainWindowStyles(object):
"""Simple Python class to hold style-related configurations for widgets."""
Frame = dict(
bg="#000000",
)
BaseLabel = dict(
font="Verdana 8",
)
Label = dict(
bg="#000000",
fg="#CCCCCC",
**BaseLabel
)
ConnectedLabel = dict(
bg="#000000",
fg="#00FF00",
**BaseLabel
)
BaseFormCtrl=dict(
highlightthickness=0, # Removes stupid border around the widget
)
BaseEntry = dict(
insertwidth=1,
selectborderwidth=0,
selectbackground="#0099FF",
font="Verdana 8",
**BaseFormCtrl
)
Entry = dict(
bg="#FFFFFF",
fg="#000000",
disabledbackground="#000000",
disabledforeground="#666666",
insertbackground="#000000",
**BaseEntry
)
DarkEntry = dict(
bg="#000000",
fg="#CCCCCC",
insertbackground="#FFFFFF", # Text insertion blinking cursor
**BaseEntry
)
Listbox = dict(
bg="#000000",
fg="#CCCCCC",
**BaseFormCtrl
)
Dialogue = dict(
bg="#000000",
fg="#CCCCCC",
#disabledbackground="#000000",
#disabledforeground="#CCCCCC",
wrap=tk.WORD,
state="disabled",
**BaseEntry
)
Button = dict(
bg="#000000",
fg="#CCCCCC",
activebackground="#000000",
activeforeground="#0099FF",
**BaseFormCtrl
)
# If using the Tkinter scrollbar, uncommon these. If using the ttk
# scrollbar, use ttk's theming system instead.
Scrollbar = dict(
#relief="flat",
#troughcolor="#000000",
#bg="#606060",
#activebackground="#999999",
#borderwidth=1,
#width=12,
#highlightthickness=0,
)
class Scrolled(object):
"""My own implementation for adding a scrollbar to a widget. Similar in
principal to Python's ScrolledText module, but it works on other widgets too
(this script uses it on Listbox too). So it's more like the Perl/Tk module
Tk::Scrolled in that it can wrap any widget, in theory."""
def __init__(self, master, widget_class, attributes=None, scrollbar=None):
"""
master is the parent widget
widget_class is the class, like Text or Listbox
attributes are attributes for the widget
scrollbar are attributes for the scrollbar
"""
if attributes is None:
attributes = []
if scrollbar is None:
scrollbar = []
self.master = master
# Parent frame to hold the widget + scrollbar
self.frame = Frame(master)
# The scrollbar
self.scrollbar = Scrollbar(self.frame, **scrollbar)
# The widget itself
self.widget = widget_class(self.frame,
yscrollcommand=self.scrollbar.set,
**attributes
)
self.scrollbar.configure(command=self.widget.yview)
self.scrollbar.pack(side="right", fill="y")
self.widget.pack(side="right", fill="both", expand=1)
def widget(self):
"""Get at the inner widget."""
return self.widget
def scrollbar(self):
"""Get at the scrollbar widget."""
return self.scrollbar
def pack(self, **kwargs):
"""Wrapper so that pack() works as you'd expect."""
self.frame.pack(**kwargs)
def resize_and_center(win, width, height):
"""Resize a window and center it on the screen."""
screen_w = win.winfo_screenwidth()
screen_h = win.winfo_screenheight()
geometry = "{}x{}+{}+{}".format(
width,
height,
screen_w / 2 - width / 2,
screen_h / 2 - height / 2,
)
win.geometry(geometry)
if __name__ == "__main__":
app = ChatClient()
app.start()
There are 2 comments on this page. Add yours.
Really cool bro.. i'll take a look at your code! ty very much for sharing :D
Thanks for the code :)
0.0114s
.