Once a web browser has downloaded a web page, it has to show that web page to the user. Since we're not savages,For most of 2011, I mostly used the command-line
w3m browser. It built character. we browse the web through a graphical user interface. How? In this chapter I'll equip my toy browser with a graphical user interface.
Desktop and laptop computers run operating systems that provide desktop environments, with windows, icons, menus, and a pointer.Sometimes terminal diehards acronym this to “WIMP environment” as a snide insult. So in order to draw to the screen, a program communicates with the desktop environment:
Though the desktop environment is responsible for displaying the window, the program is responsible for darwing its contents. Applications have to redraw these contents sixty times per second or so for interactions feel fluid,On older systems, applications drew directly to the screen, and if they didn't update, whatever was there last would stay in place, which is why in error conditions you'd often have one window leave “trails” on another. Modern systems use a technique called compositing to avoid trails (at the cost of using more memory), but applications must still redraw their window contents to change what is displayed. and must respond quickly to clicks and key presses so the user doesn't get frustrated.
Doing all of this by hand is a bit of a drag, so programs usually use a graphical toolkit to simplify these steps. These toolkits allow you to describe your program's window in terms of widgets like buttons, tabs, or text boxes, and take care of drawing and redrawing the window contents to match that description.
Python comes with a graphical toolkit called Tk using the Python package
tkinter.The library is called Tk, and it was originally written for a different language called Tcl. Python contains an interface to it, hence the name. Using it is quite simple:
Here we call
tkinter.Tk() to create a window, and
tkinter.mainloop() to start the process of redrawing the screen. Internally, when we call
tkiner.Tk(), Tk is communicating with the desktop environment to create the window, and returns an identifier for that window, which we store in the
window variable. When we call
tkinter.mainloop(), Tk is entering a loop that internally looks like this:This happens in the function
tkCmds.c in the Tcl/Tk source code. That code is more complex to handle interrupts and errors.
pendingEvent asks the desktop environment for any recent events, like mouse clicks or key presses,
handleEvent determines what functions in our code to call in response to that event, and
drawScreen draws the various widgets. Applications that use the graphical toolkit extend
handleEvent to draw interesting stuff on the screen and to react when the user clicks on that stuff.
This event loop pattern is common in many applications, from web browsers to video games. Our simple window above does not need much event handling (it ignores all events) or much drawing (it is a uniform white or gray). But in more complex graphical applications the event loop pattern makes sure that all events are eventually handled and the screen is eventually updated, both of which are essential to a good user experience.
Our graphical browser will begin by writing the web page text to a canvas, a rectangular widgets that we can draw circles, lines, and text in.You may be familiar with the HTML
<canvas> element, which is a similar idea: a 2D rectangle on which you can draw shapes. Tk also has widgets like buttons and dialog boxes, but for writing a browser we will need the more fine-grained control over appearance that a canvas provides.This is why desktop applications are more uniform than web pages: desktop applications generally use the widgets provided by a common graphical toolkit, which limits their creative possibilities.
tkinter.Canvas creates a canvas in Tk; we can package that up with
tkinter.Window in the following function:
The first line creates the window, as above; the second creates the
Canvas inside that window. We pass
Canvas some arguments that define its size; I chose 800×600 because that was a common old-timey monitor size.This size, called Super Video Graphics Array, was standardized in 1987, and probably did seem super back then. The third line is a Tk peculiarity, which positions the canvas inside the window.
Once you’ve made a canvas, you can call methods that draw shapes on the canvas:
You ought to see a rectangle, starting near the top-left corner of the canvas and ending at its center; then a circle inside that rectangle; and then the text “Hi!” next to the circle.
Play with the arguments to those methods to figure out which coordinate each argument refers; the right answers are in the online documentation. It is important to remember that coordinates in Tk, like (10, 20), refer first to X position from left to right and then to Y position from top to bottom. This means that the bottom of the screen has larger Y values, the opposite of what you might be used to from math.
Now that we've got a GUI window with a canvas, let's draw a simple web page on it.
Remember that in the last chapter, we implemented a simple function that stepped through the web page source code character by character and printed the text (but not the tags) to the console window. Now we want to print the characters to our GUI instead.
To start, let's change the
show function from the previous chapter into a function that I'll call
lexForeshadowing future developments… which just returns the text-not-tags content of an HTML document, without printing it:
Now we can write a new
show function that takes the text (with tags removed) as an input, and draws it to the screen, first setting up the window as above and then executing this loop:
Now, let's refactor
show to output to our window instead.
show will have to start by creating the window and canvas, like above, and then drawing the text on the screen, character by character:
Let's test this code to a real webpage, and for reasons that might seem inscrutibleIt's to delay a discussion of basic typography to next class…, let's test it on this first chapter of 西游记 or "Journey to the West", a classic Chinese novel about a monkey. Run this URLRight click on the link and "Copy URL". through
request,If you're in the US, you'll probably see this phase take a while: China is far away!
show, you should see a window with a big blob of black pixels roughly 100 pixels from the top left corner of the window.
Why a blob instead of letters? Well, of course, because we are drawing every letter in the same place, so they all overlap! Let's fix that:
Now the characters form a line from left to right, and individual characters are readable. But with an 800 pixel wide canvas and 13 pixels per character, you can only fit about 60 characters. You need more than that to read a novel, so we now need to wrap the text once we reach the edge of the screen:In the olden days of type writers, going to a new line would be two operations: to move down the page you would feed in a new line, and then you'd return the carriage that printed letters to the left margin. You can see the same two operations below. When ASCII was standardized, they added separate characters for these operations: CR and LF. That's why headers in HTTP are separated by
\r\n, or CR followed by LF, even though computers have nothing mechanical inside that necessitates separate operations. In most contexts, however, you generally just use
\n create a new line.
Here, when we get past pixel 787 to the rightNot 800, because we started at pixel 13 and I want to leave an even gap on both sides. we increase y and reset x to the left hand side again. This moves us down a line and makes it possible to see all of the text. Also, note that I've got some magic numbers here: 13 and 18. I got these from font metrics, which are introduced in the next chapter.
Now we can read several paragraphs of text, but there's still a problem. But if there's enough text, all of the lines of text don't fit on the screen, and there's still content you can't read. Usually users scroll the page to look at different parts of it.
Scrolling introduces a layer of indirection between page coordinates (this text is 132 pixels from the top of the page) and screen coordinates (this text is 72 pixels from the top of the screen). Generally speaking, a browser lays out the page—determines where everything on the page goes—in terms of page coordinates and then renders the page—draws everything—in terms of screen coordinates.
Our browser will have the same split. Right now
show both computes the position of each character and draws it: layout and rendering. Let's split it into a
layout function that just computes the position of each character, and a
render function that creates the window and draws each character on it. This way,
layout can operate with page coordinates and only
render needs to think about screen coordinates.
Let's start with
render needs to know which character to place where, which is what
layout computes, so let's have
layout return a list of tuples: the character to draw and its
render then loops through that list and draw each tuple:
I am calling the output of
layout a display list, since it is a list of things to display; the term is standard. There's no scrolling yet, but before we add it, let's write
layout is much like the character-by-character loop in the existing
show function, but when instead of
canvas.create_text on each character it adds it to a list:
show split, we can implement scorlling. To scroll the page by, say, 100 pixels, we use
y - 100 in place of
y when we call
create_text. If the
scroll variable stores how much to scroll the page, this is:
If you change the value of
scroll the page will now scroll up and down. But how does the user change
Most browsers scroll the page when you press the up and down keys, rotate the scroll wheel, or drag the scroll bar. To keep things simple, let's stick to one: the down key.
Tk allows you to bind a function to a key, which instructs Tk to call that function when the key is pressed. For example, to call the
scrolldown function when the "Down" button is pressed, we write:
Then we need
scrolldown to increment
y and re-draw the canvas:
There are some pretty big changes here. First, I've moved the loop that draws all the text into a function,
draw. That function is called by
render directly, when the page is first rendered, but also by
scrolldown, so that the page can be redrawn after scrolling.The
nonlocal scroll line is a Python quirk. It tells Python to look for an existing
scroll variable instead of defining a new one.
If you try this out, you'll find that scrolling draws all the text a second time. That's because we didn't erase the old text when we started drawing the new text. We call
canvas.delete at the top of
The last chapter build a simple command-line browser. We've now upgraded it to a rudimentary graphical user interface that can be scrolled.
This graphical browser works well on a Chinese web pages, but it looks weird on English ones. All of the characters are spaced far apart, lines break in the middle of words, and there's no support for paragraphs, links, or formatting. We'll fix these problems in the next chapter.
Canvasconstructor. Change the canvas to have a white background and give it a red border (which Tk calls a highlight). This will help you see where the edge of the canvas is. if you resize the window.
layoutto handle newlines by ending the line and starting a new one. Increment y by more than 18 to give the illusion of paragraph breaks. You should be able to see some poems embedded in "Journey to the West".
drawto skip characters outside the 800×600 window. Users can't see those characters anyway. Make sure you use screen, not page, coordinates to determine which caracters to draw! This should make scrolling noticable faster for "Journey to the West".We'll return to this optimization in chapter 10.
canvas.packcall and bind to the
<Configure>event to run code when the window is resized. You can get the new window width and height with the
heightfields on the event object. Remember that when the window is resized, the line breaks will change, so you will need to call