Xlib tutorial part 9 -- Buttons

by Alan at Mon 9th Mar 2009 1:00AM EST

Hello, welcome to section 9 of this Xlib tutorial. In this lesson we're going to start creating buttons for your users to press. We're going to build on where we left off last lesson with XDrawString16. Also we're going to start encapsulating the code that surrounds objects in our window.

The place to start is our main loop. It's quite different. The code is below. The first thing to notice is there is now no drawing code inside the main loop anymore. It's been completely abstracted away. Instead, all the parts of the switch statement just dispatch to something called a Button. The second thing to notice is that the font info, GC, width and height and the text to draw are no longer passed. Instead we have something called an XContext.


	...
int main_loop(Display *dpy, XContext context){
	XEvent ev;

	/* as each event that we asked about occurs, we respond. */
	while(1){
		Button *button = NULL;
		XNextEvent(dpy, &ev);
		XFindContext(ev.xany.display, ev.xany.window, context, (XPointer*)&button);
		switch(ev.type){
		/* configure notify will only be sent to the main window */
		case ConfigureNotify:
			if (button)
				buttonConfigure(button, &ev);
			break;
		/* expose will be sent to both the button and the main window */
		case Expose:
			if (ev.xexpose.count > 0) break;
			if (button)
				buttonExpose(button, &ev);
			break;

		/* these three events will only be sent to the button */
		case EnterNotify:
			if (button)
				buttonEnter(button, &ev);
			break;
		case LeaveNotify:
			if (button)
				buttonLeave(button, &ev);
			break;
		case ButtonRelease:
			if (button && button->buttonRelease)
				button->buttonRelease(button->cbdata);
			break;
		}
	}
}
	...

	

An XContext can be treated just like a hash table, the only proviso is that you can only use XID's (windows, pixmaps, GContexts, ...) as the key. The XContext is designed specifically for dispatching to the appropriate object when an event comes in. Call the XFindContext function with the context and the window id, and you get back what you saved. We'll see the call to XSaveContext a little later.

The rest of the changes to the code might make more sense if you had seen how Button is defined.


	...
typedef void (*Callback)(void *cbdata);

typedef struct Button Button;
struct Button {
	XChar2b * text;
	int text_width;
	int font_ascent;
	int width, height;
	unsigned long border, background, foreground;
	void *cbdata;
	Callback buttonRelease;
};
	...
	

Notice that the text to display is here, its width, how tall it should be, and its colours. The buttonRelease callback and cbdata is so that our setup code can pass a function to be called when a click has happened.

So, next let's look at what happens when some of the button functions are called.


		...

void buttonExpose(Button *button, XEvent *ev) {
	int textx, texty, len;
	if (!button) return;
	if (button->text){
		len = XChar2bLen(button->text);
   		textx = (button->width - button->text_width)/2;
   		texty = (button->height + button->font_ascent)/2;
   		XDrawString16(ev->xany.display, ev->xany.window, DefGC(ev->xany.display), textx, texty,
			button->text, len);
	} else {  /* if there's no text draw the big X */
		XDrawLine(ev->xany.display, ev->xany.window, DefGC(ev->xany.display), 0, 0, button->width, button->height);
		XDrawLine(ev->xany.display, ev->xany.window, DefGC(ev->xany.display), button->width, 0, 0, button->height);
	}
}
		...
	

This should be straightforward. DefGC is a macro, I've defined, that gets the default GC that Xlib creates for us during XOpenDisplay(). We really should have been using it from the beginning. The other thing that might be different from before is that we're now using the event object to get the dispaly and window, and from there the GC. The reason is that this way we know we're drawing in the window on the display that was exposed.


		...
#define DefGC(dpy) DefaultGC(dpy, DefaultScreen(dpy))
		...
	

That's a macro that expands to two macros (that are part of Xlib).


		...
void buttonConfigure(Button *button, XEvent *ev){
	if (!button) return;
	if (button->width != ev->xconfigure.width
			|| button->height != ev->xconfigure.height) {
		button->width = ev->xconfigure.width;
		button->height = ev->xconfigure.height;
		XClearWindow(ev->xany.display, ev->xany.window);
	}
}
		...
	

buttonConfigure just records the new size of the button if it has changed. Notice that the X calls the button a window. And it is. It's a subwindow of the main appliation window. Each rectangular piece of your screen can be a window. XClearWindow() verifies that the old version of the window was cleared and we won't be drawing over text later.


		...

void buttonEnter(Button *button, XEvent *ev) {
	XSetWindowAttributes attrs;
	if(!button) return;
	attrs.background_pixel = button->border;
	attrs.border_pixel = button->background;
	XChangeWindowAttributes(ev->xany.display, ev->xany.window,
			CWBackPixel|CWBorderPixel, &attrs);
	XClearArea(ev->xany.display, ev->xany.window, 0, 0, button->width, button->height, True);
}
		...
	

This is called when the mouse enters the button. In this case, we have it switch its border and background colours.


		...
void buttonLeave(Button *button, XEvent *ev) {
	XSetWindowAttributes attrs;
	if(!button) return;
	attrs.background_pixel = button->background;
	attrs.border_pixel = button->border;
	XChangeWindowAttributes(ev->xany.display, ev->xany.window,
			CWBackPixel|CWBorderPixel, &attrs);
	XClearArea(ev->xany.display, ev->xany.window, 0, 0, button->width, button->height, True);
}
		...
	

and we switch them back when the mouse leaves. XChangeWindowAttributes lets us change the border and background colours of a window, among other things. XClearArea like XClearWindow clears the window to make sure the background colour is updated. It won't be until the X display server has to clear the window in the case of an expose event. In this case, we cause an expose event by passing True as the last argument of XClearArea.

So now that we've seen what happens when events come in to the button, let's consider how this button got created in the first place.


		...
void createButton(Display *dpy, Window parent, char *text, XFontStruct *font,
		int x, int y, int width, int height,
		unsigned long foreground, unsigned long background, unsigned long border,
			XContext ctxt, Callback callback, void *cbdata){
		...
	

That's a large number of arguments. From what we've talked about in previous lessons and from the mention of the XContext and Callback from before, you should be able to understand what each of them is for.


		...
	Button *button;
	Window win;
	int strlength = strlen(text);

	win = XCreateSimpleWindow(dpy, parent, x, y, width, height,
		2, border, background); /* borderwidth, border and background colour */
	if (!win) {
		fprintf(stderr, "unable to create a subwindow
");
		exit(31);
	}

	button = calloc(sizeof(*button), 1);
	if (!button){
		fprintf(stderr, "unable to allocate any space, dieing
");
		exit(32);
	}
		...
	

Here's where the button object is created. If we were were in C++ or some other language with direct support for objects we would like say button = new Button( ...args...); but we're using C right now. So the code below is setting all the fields.


		...

	button->font_ascent = font->ascent;

	button->text = malloc(sizeof(*button->text) * (strlength+1));
	if (!button->text){
		fprintf(stderr, "unable to allocate any string space, dieing
");
		exit(32);
	}
	strlength = utf8toXChar2b(button->text, strlength, text, strlength);
	button->text_width = XTextWidth16(font, button->text, strlength);
	button->buttonRelease = callback;
	button->cbdata = cbdata;
	button->width = width;
	button->height = height;
	button->background = background;
	button->foreground = foreground;
	button->border = border;

	XSelectInput(dpy, win,
		ButtonPressMask|ButtonReleaseMask|StructureNotifyMask|ExposureMask
			|LeaveWindowMask|EnterWindowMask);

	XSaveContext(dpy, win, ctxt, (XPointer)button);
	XMapWindow(dpy, win);
}
		...
	

Notice the new Masks being sent to XSelectInput(). We want to know about a entry and exit from the button so that we can highlight it when the user mouses over it.

The other thing to see here is the call to XSaveContext(). Contexts are provided by Xlib. As mentioned above they can be used just like hash tables for Window ids which is exactly what we're using them here for.

What's left that's changed? Our setup() function.


		...
XContext setup(Display * dpy, int argc, char ** argv){
	static XrmOptionDescRec xrmTable[] = {
		{"-bg", "*background", XrmoptionSepArg, NULL},
		{"-fg", "*foreground", XrmoptionSepArg, NULL},
		{"-bc", "*bordercolour", XrmoptionSepArg, NULL},
		{"-font", "*font", XrmoptionSepArg, NULL},
	};
	Button *mainwindow;
	Window win;
	XGCValues values;

	XFontStruct * font;
	XrmDatabase db;

	XContext ctxt;

	ctxt = XUniqueContext();

	mainwindow = calloc(sizeof(*mainwindow), 1);
		...
	

I've moved the xrmTable into the setup function. It's only ever used in this function and it doesn't really matter where it is, but might as well not pollute the file level namespace.

The above code also treats the main window as a button, and creates a context for all our various windows. (In this case 2, but more in subsequent sections). Notice we create a new object to store the mainwindow.


		...

	XrmInitialize();
	db = XrmGetDatabase(dpy);
	XrmParseCommand(&db, xrmTable, sizeof(xrmTable)/sizeof(xrmTable[0]),
		"xtut9", &argc, argv);

	font = getFont(dpy, db, "xtut9.font", "xtut9.Font", "fixed");
	mainwindow->background = getColour(dpy,  db, "xtut9.background", "xtut9.BackGround", "DarkGreen");
	mainwindow->border = getColour(dpy,  db, "xtut9.border", "xtut9.Border", "LightGreen");
	mainwindow->foreground = values.foreground = getColour(dpy,  db, "xtut9.foreground", "xtut9.ForeGround", "Red");


	mainwindow->width = 400;
	mainwindow->height = 400;
		...
	

The above code should all be straight forward. The only major differnce from earlier is that we're storing the colours in the mainwindow object.


		...

	win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), /* display, parent */
		0,0, /* x, y: the window manager will place the window elsewhere */
		mainwindow->width, mainwindow->height, /* width, height */
		2, mainwindow->border, /* border width & colour, unless you have a window manager */
		mainwindow->background); /* background colour */

	Xutf8SetWMProperties(dpy, win, "XTut9", "xtut9", argv, argc,
		NULL, NULL, NULL);

	/* make the default pen what we want */
	values.line_width = 1;
	values.line_style = LineSolid;
	values.font = font->fid;

	XChangeGC(dpy, DefGC(dpy),
		GCForeground|GCLineWidth|GCLineStyle|GCFont,&values);
		...
	

We're using the default GC again and setting it to our prefered configuration. Whenever possible, reuse things like GCs since it uses less resources on the server.

To make sense of the next section, we need to introduce a new structure definition and callback function (for when the button is pressed).


		...   (At top level)

typedef struct exitInfo ExitInfo;
struct exitInfo {
	Display *dpy;
	XFontStruct *font;
};

void exitButton(void *cbdata){
	ExitInfo *ei = (ExitInfo*)cbdata;
	XFreeFont(ei->dpy, ei->font);
	XCloseDisplay(ei->dpy);
	exit(0);
}

		...
	

In the exit function we free the font. The X display server will do that for us anyway when we close the connection, but it's good practice to think about making sure we free things when we're done with them.

So, now that we have those definitions the following should make sense.


		...

	{
	ExitInfo *exitInfo;
	exitInfo = malloc(sizeof(*exitInfo));
	exitInfo->dpy = dpy;
	exitInfo->font = font;
	createButton(dpy, win, "Exit", font, /*display text font */
		mainwindow->width/2-40, 17, 80, (font->ascent+font->descent)*2,/*xywh*/
			/* colours */
		mainwindow->foreground, mainwindow->background, mainwindow->border,
		ctxt, exitButton, exitInfo);			/* context & callback info */
	}
		...
	

Create the callback data, and then create the button and give it the callback data.

When the button in our main window is created, we can finish the setup call.

		...

	/* tell the display server what kind of events we would like to see */
	XSelectInput(dpy, win, StructureNotifyMask|ExposureMask);
	/* okay, put the window on the screen, please */
	XMapWindow(dpy, win);

	/* save the useful information about the window */
	XSaveContext(dpy, win, ctxt, (XPointer)mainwindow);

	return ctxt;
}
		...
	

XSelectInput() takes a few less masks than earlier since we no longer have to wonder about button clicks. And we save the mainwindow the same way as we saved the button with XSaveContext() so it can be retrieved in our main loop.

For completeness, here's the XChar2bLen that was called in buttonExpose.

		...

int XChar2bLen(XChar2b *string){
	int j = 0;
	for(j = 0; string[j].byte1 || string[j].byte2; j ++ )
			;
	return j;
}
		...
	

And here's our new main().


		...

int main(int argc, char ** argv){
	Display *dpy;
	XContext ctxt;

	/* First connect to the display server */
	dpy = XOpenDisplay(NULL);
	if (!dpy) {fprintf(stderr, "unable to connect to display
");return 7;}
	ctxt = setup(dpy, argc, argv);
	return main_loop(dpy, ctxt);
}
		...
	

We're no longer passing as much information around, just a reference to the hashtable with the display connection.

Here's the full code.

A few things to try:

Comments are closed.