Learning Drupal 6 Module Development
上QQ阅读APP看书,第一时间看更新

Creating a Custom Theme

In this section, we will investigate a few strategies for creating custom themes. We will create a custom theme, initially changing only CSS stylesheets, and then move on to a more sophisticated theme, making use of custom templates. Finally, we will create some PHP code that works more closely with the theme system itself. We will even implement a theme hook to provide a higher degree of customization for our theme.

Organization of Themes

In Drupal 6, themes are organized similarly to modules. Inside the main Drupal directory is a themes/ folder, which in turn has a number of subfolders, each named after the theme it provides:

Organization of Themes

These are the main "top level" themes that come with Drupal 6: Bluemarine, Chameleon, Garland, and Pushbutton. Later in the book, we will talk about sub-themes, (or derivative themes,) which are not visible here at the top level. By convention, they exist in subfolders within the "top level" theme that they rely upon.

Note

Theme engines are stored in the drupal/themes/engines directory. By default, the only folder inside of drupal/themes/engines is phptemplate/.

Inside each theme directory are the files necessary for the theme to function. Each theme must have a theme .info file. Those that use the PHPTemplate engine have template files whose names end with the .tpl.php extension.

Stylesheets (particularly style.css), images, and other files will also be stored inside the theme directory. If there are a lot of these supporting files, the theme developers may have chosen to further organize the theme folder into subfolders.

Finally, some themes make use of a template.php file, which the PHPTemplate engine automatically loads if present. This file can be used to provide additional theming in the form of PHP functions.

Sub-themes (Derivative Themes)

Drupal 6 offers a new feature that developers call theme inheritance, a concept borrowed from Object-Oriented Programming. Roughly speaking, the idea is that a new theme can be created that draws upon the resources of another (parent) theme. This new derivative theme can make use of files, images, stylesheets, and other resources of the parent. But it can also strategically "override" or augment the parent theme.

Overriding occurs when a derivative theme supplies a new behavior or configuration that is designed to replace a behavior or configuration in the parent. For example, if a parent stylesheet specifies that the borders on all block elements are blue, a child might override this, declaring that all block elements be surrounded by a green border instead. Overriding need not be restricted to CSS statements, though; PHPTemplates, entire stylesheets, JavaScript files, and images can all be overridden.

But sub-themes can do more than override existing elements. They can add new resources—new images, styles, or scripts—without overtly changing the parent.

Note

Theme inheritance is one of many instances of the Drupal developers taking the principles behind object-oriented programming and successfully adapting them to Drupal's architecture.

In the theme examples seen here, we only deal with one layer of derivative themes. However, multiple levels of inheritance are supported. A parent theme can have a sub‑theme, which in turn may have another sub-theme. Nesting themes this way, though, may lead to maintenance headaches, and should be done only when it is clearly the best choice.

Creating sub-themes themes has some obvious benefits. It is faster than creating a new theme from scratch. Less code is duplicated. Sets of similar (but different) themes can be created quickly and easily. In short, derivative themes ease the burden of the theme developer.

But there is a significant gotcha to be wary of. A sub-theme depends on its parent. But what happens when a parent theme is changed? A changed stylesheet, template, or JavaScript file in a parent module might lead to unpredictable behavior in derivative themes. One should particularly be aware of this possibility when basing a sub-theme on a parent theme that is maintained by someone else.

Now that we have a bit of theory under our belts, let's take a look at the themes provided with the Drupal core.

How Each Theme Functions

You can log into Drupal and check out the Administer | Site building | Themes page and check out the screenshots to find out how each theme looks. But we are more interested in how they function. Here's a brief overview of the architecture of each module. Derivative Marvin and Minelli themes are listed here as second-level bullets beneath the top-level themes they use.

  • Bluemarine: The Bluemarine theme is a very simple example of a PHPTemplate-based theme. It uses HTML tables and CSS for layout, and it makes very little use of images.
  • Chameleon: The Chameleon theme was mentioned earlier because it does not make use of any theme engine. Instead, the chameleon.theme file (found in drupal/themes/chameleon) contains PHP code that implements half a dozen different theming hooks.
    • The derivative Marvin theme makes use of chameleon. Marvin is located in drupal/themes/chameleon/marvin.
  • Garland: Garland is the default Drupal 6 theme. It is powerful, but also complex. It makes use of the PHPTemplate engine. Along with the usual features, it includes support for configuring custom color schemes (see Administer | Site building | Themes and click the configure link next to the Garland theme). Along with the normal PHP templates, Garland has some specialized theming functions in the drupal/themes/garland/template.php file.
    • Minelli: The Minelli theme is a derivative theme based on Garland. Since it borrows all of the PHP-coded functionality from its parent, it has the same features as Garland (such as the color chooser).
  • Pushbutton: Like Bluemarine and Garland, Pushbutton uses the PHPTemplate engine. While it makes heavy usage of images, it is not much more complex than the Bluemarine theme.

This brief overview should provide food for thought. Here are six very different themes. One does not use templates at all. Two are sub-themes. One, Garland, makes use of a sophisticated directory structure, theme-specific PHP functions, and other advanced features. Another, Bluemarine, is a minimal PHPTemplate module with only one image, a couple of stylesheets, and a handful of template files. Together, these modules illustrate the flexibility of the Drupal theme system.

We will base our themes on the Bluemarine theme. Its simplicity makes it the optimal starting point for exploration. Leveraging theme inheritance, we will create a custom derivative theme.

Creating a Theme

Rather than creating a full-fledged theme, we will make a sub-theme based on Bluemarine. This makes it easy for us to strategically modify a few files without the need to create a full set of templates.

As mentioned above, a derivative theme draws on the functionality of an existing theme. We can be very selective about which files we create. For example, we could just make a few stylesheet modifications, or just change around a few images. In fact, we could do all of this without touching a single PHP file.

In this first part, we will create a new sub-theme and manipulate the style.css stylesheet. We will also add a new background image.

Creating the Theme Directory

The first step in creating a theme is to create the directory to house the new theme. Where do we put this directory?

Earlier, we saw the convention of placing the derivative theme inside the parent theme's directory.

However, in the previous chapter, we discussed the conventions for modules: custom modules belong in drupal/sites/all/modules. This would indicate that we should store themes in drupal/sites/all/themes.

So which is the case?

Most of the time, themes should follow an organizational convention similar to that used for modules. There are two reasons why it is best to keep themes under the drupal/sites/all/themes directory (or, for site-specific configuration, under drupal/sites/<sitename>/themes):

  • This method maintains a degree of separation between the official Drupal code and the custom code, making it easier to maintain.
  • While Drupal upgrades might change the core themes (including, perhaps, recreating or removing directories), if custom themes are outside the core themes directory, they will not be touched during upgrades.

There is one notable exception, though. In the case where the same theme author or authors make use of theme inheritance to create multiple themes, it may be best to maintain them in the sort of directory structure employed by Drupal core themes.

In our case, we are extending a theme that we did not create. Clearly, we would do best to put our module in the drupal/sites/all area.

When Drupal is first installed, there will be no themes/ directory there. The directory may need to be created, and in rare cases, the permissions on the directory may need to be set so that they are readable by the user the web server runs as (by default, this should be the case already).

We are going to create a new theme called Descartes (descartes), named after the famous early modern philosopher. We will put it in the drupal/sites/all/themes folder.

Creating the Theme Directory

All our theme files go in this new directory.

A .info File

The next thing our new Descartes theme needs is a .info file. This file provides Drupal with information about the theme. Some of that information is simply used for display, while other directives provide Drupal with information necessary for serving and executing the theme code.

Just as with a module's .info file, this file should be named with the machine-readable name of the theme. Thus, our file is named descartes.info.

Also like the module's .info file, this file is in the PHP configuration format, and it has many of the same directives.

Here is our theme configuration file:

; $Id$
name = Descartes
description = Table-based multi-column theme with a Cartesian flavor.
version = 1.0
core = 6.x
base theme = bluemarine

There is very little to note in this simple file. The first four fields function in the same capacity for themes as they do for modules.

The name field should contain the user-friendly title of the theme, and the description should provide a one-line description of the theme. Likewise, version should give the version number of the theme.

The most interesting directive, though, is the base theme directive. This tells Drupal that this theme makes use of resources from another theme—the bluemarine theme. It is this directive that effectively makes this a sub-theme of Bluemarine. The name passed to the base theme directive must be the machine-readable name.

That's all there is to our first theme .info file. There are, of course, many other directives that can be used in a theme's .info file. We will look at a few more of these later in the chapter. A complete list is available at: http://drupal.org/node/137062.

Now we are ready to create a custom stylesheet.

A CSS Stylesheet

The first step in creating a new stylesheet is answering a question—How divergent is this theme's style going to be from the parent style?

We are basing our Descartes theme on the Bluemarine theme. But how similar will style elements be? Will we keep the same fonts? The same colors? The same paddings, borders, and margins? The degree to which the styles differ will determine how we create our stylesheet.

Let's take a quick glance at our site with the Bluemarine style:

A CSS Stylesheet

How much will our new style differ from this one?

If our new style is to be radically divergent from the Bluemarine style, then we might want to create a new style.css file (or copy over the style.css from Bluemarine and begin a major overhaul).

However, the stylistic changes we will make here are minor. We will change the look and feel of the left-hand column, turn the right-hand column on again (it is off by default), and change some of the colors.

Since these changes can be accomplished with just a dozen or so lines of CSS, we will take a different course of action. Instead of copying the entire style.css file from Bluemarine, we will create a new stylesheet that overrides some of Bluemarine's settings.

As of now, there is no convention for naming these auxiliary stylesheets, so we will go with the uninspiring name new.css. Following are the contents of this CSS file:

/*
** Styles to override the styles.css in
** bluemarine.css
** $Id$
*/
/*
* Plain white right nav.
*/
#sidebar-right {
border-left: 1px solid #ea940c;
background-color: white;
}
/*
* Add background image.
*/
#sidebar-left {
background: url(leMonde_sidebar.png) no-repeat top left;
border-right: 1px solid #ea940c;
}
/*
* Set the background for the mission.
*/
#mission {
background-color: #ea940c;
padding: 5px 15px 5px 15px;
text-align: center;
}

Here we are adding styles to three specific IDs in the HTML: sidebar-right, which identifies the right-hand sidebar; sidebar-left, identifying the left-hand sidebar; and mission, which identifies the box containing the site's mission ("All philosophy is a footnote to Plato").

The right-hand sidebar in the Bluemarine theme is battleship gray by default. We want it to be white with an orange (#ea940c) border on the left side.

On the left-hand side, we want something more ornate: a background image (leMonde_sidebar.png). Before this image can be successfully used, it must be copied into our module directory.

Note

Make sure you copy any necessary images into the theme directory. Even if you are using images from the parent theme, it is best to copy those images into the derivative theme.

As in our left-hand sidebar, we want an orange border to set this off from the main page content.

Note

CSS and URLs

Theme URLs are resolved relative to the theme directory. For example, the CSS statement url(leMonde_sidebar.png) will attempt to find the file drupal/sites/all/themes/descartes/leMonde.png.

Finally, we want to fix up the display of the mission statement by reducing the padding, changing the background color, and aligning the text to the center of the box instead of the left-hand side.

With new.css created, we have only one more thing to do. We need to direct Drupal to use our new CSS in addition to the style.css of the parent. This is done by adding the following lines to our descartes.info file:

stylesheets[all][] = style.css
stylesheets[all][] = new.css

These two lines provide explicit instructions to load the style.css and new.css stylesheets.

What's the idea with all the square brackets? Square bracket notation was introduced in Drupal 6 as a method for specifying multiple values for a directive in a .info file. They function analogously to arrays in PHP.

The stylesheet directives above have two sets of square brackets apiece: stylesheet[all][]. The first set, [all], indicates which media type of pages the given stylesheet should be used in.

Those familiar with CSS will recognize the term 'media type'. It refers to the output medium that the stylesheet is used for. Common media types are screen (for a device with a screen) and print (for printed pages). The media type all applies to any media type the browser requests.

Using the media type, then, we could set special stylesheets for printer-friendly pages:

stylesheets[print][] = printed.css

The Garland theme, for example, has printer-specific CSS files.

The second pair of square brackets, [], function like the array assignment syntax for PHP. Just as $my_array[] = 'foo' places the string foo at the last place in the array, so the directive stylesheets[all][] = 'new.css' puts new.css at the end of the stylesheets list.

Note

Clearing the template cache

When developing new templates, you may need to clear the template cache before the latest theme changes show up. To do this, use the Empty cache item in the Devel module's module development block. See Chapter 1 for more information on the Devel module.

A CSS Stylesheet

With these modifications made to the theme's .info file, we are ready to take a look at the fruits of our labor.

Since our theme is located in the drupal/sites/all/themes directory and has a .info file, Drupal should automatically find it. All we need to do is go to Administer | Site building | Themes and enable the theme, setting it to default for testing.

You may notice that the screenshot of our Descartes theme shows the Bluemarine theme instead. That's because we haven't yet created our own screenshot image (screenshot.png in the theme directory). Drupal is "inheriting" the parent theme's screenshot.

Once the theme is enabled, we can navigate to the home page to see how it looks.

Tip

Do not set the administration theme to a theme in the development or testing phase. Doing so can result in an unfortunate predicament. Errors in the theme files can prevent you from accessing the administration screens. Only stable themes should be used on the administration side of Drupal.

Our new site should look something like the following screenshot:

A CSS Stylesheet

While many of the elements of this design look the same as they did in the Bluemarine theme, our changes should be immediately evident—different left and right sidebars, a background image in the left-hand sidebar, and a lot more orange.

We have just successfully created a working derivative theme. All of this was done with just a handful of files—and no PHP coding. A final glance at the Descartes theme directory shows only four files:

A CSS Stylesheet

The logo.png file was simply copied over from the Bluemarine theme. We created only three new files for this theme.

However, sometimes a little CSS isn't enough to attain the desired look and feel. Sometimes we need to customize the templates themselves. How do we do that?

A PHPTemplate Theme

We could start a new theme from scratch, creating each new theme file as we go, but it is often easier to begin by copying a similar theme and modifying it.

The Bluemarine theme is table-based, following the older practice of laying out an HTML page with tables. We will start with that. But we will change it, making the right-hand navigation "float" so that text can wrap around it.

Template Structure

There are five templates in the Bluemarine theme. These five templates make an appearance in almost all PHPTemplate-based themes, as they represent the pieces that Drupal composes into a page. The templates are:

  • page.tpl.php: The main template for a page. This template should contain the main elements of an HTML page, including the<html/>, <head/> and<body/> elements. The theme system provides this template access to over 30 variables.
  • node.tpl.php: This template is responsible for displaying the contents of a node. It is passed several variables, most of which relate directly to the node being processed. For example, $title, $content, and $terms contain (respectively) the node's title, text content, and associated taxonomy tags.
  • block.tpl.php: As we saw earlier in the chapter, this template is responsible for rendering a single block. The $block object passed into this template contains most of the information necessary for generating a block, though there are a few other variables passed in as well.
  • box.tpl.php: This template paints a simple box. It is used less frequently. It has access to only three variables: $title, $content, and $region.
  • comment.tpl.php: This template is used to display comments. It is given access to about a dozen variables, which pertain to the comment, the comment poster, and the context of the posting.

    Note

    When a template is omitted from a theme (and from its parent, if it has one), Drupal uses the default theme function to render the contents. For example, when box.tpl.php is missing and a box is drawn, Drupal will use the theme_box() method to theme the box data. Such default theme functions are defined in include/theme.inc.

The main page.tpl.php template is responsible for providing the framework into which Drupal content will be rendered. Most Drupal content items, including blocks, are grouped into regions.

The main page template, then, isn't responsible for displaying individual nodes, blocks, comments, and so on. Instead, it is responsible for displaying regions.

A region is a partition of the page where content can be displayed. By default, there are five pre-defined regions:

  • header: This region describes the header area of the template. In our theme, for example, this area is located in the blue box along the top.
  • content: This describes the main area of the page. For our theme, this is the section in the center of the page.
  • left: This describes one sidebar. Typically, as the name indicates, this sidebar is located on the left side of the page (though that is, of course, contingent on the theme creator's whims.)
  • right: This describes the second sidebar, usually located on the right side.
  • footer: The footer section contains information that should usually appear after the rest of the content. In our theme, footer content would appear in the gray box running along the bottom of the page.

    Note

    Regions can be tailored to your needs. Existing regions can be omitted, and new regions can be defined. As of version 6 of Drupal, this is all done in the theme's .info file using the region[] directive. Region configuration is explained further at: http://drupal.org/node/137062.

As Drupal processes a request, the content of blocks, nodes, comments, and so on is appended to the appropriate region or regions. These regions are made accessible to the main page template in the form of eponymously named variables: $content, $header, $left, $right, and $footer.

For example, the main content of a page is stored in the $content variable made accessible to page.tpl.php. Likewise, the title is stored in $title. Hence, the page template could display the main page content with something like this:

<h1><?php print $title; ?></h1>
<?php print $content; ?>

What about blocks? The precise region that a particular block is assigned to is easily configurable. In the last chapter, we used the Administer | Site building | Blocks page to tell Drupal where we wanted our Goodreads module to be displayed. What Drupal did, under the hood, was assign the module to a region (the right region).

The page template is supplied with many other variables, as well. Some contain site information, such as the $site_name, $logo, and $site_slogan variables. Others contain references to CSS ($styles) or JavaScript ($scripts, $closure). Many of them provide template designers access to dynamically-generated pieces of content. For example, the $breadcrumb variable provides access to the breadcrumb navigation:

Template Structure

All of these variables, and there are over 30, are documented in the page.tpl.php template in Bluemarine (and the other core themes).

Armed with this information, we can now edit the template.

A Page Template for Descartes

We have already created a theme, Descartes, and added some custom CSS. Now, we are going to modify just one of the templates provided by the Bluemarine theme. We will change the page template to allow the right sidebar (the right region) to float.

The first thing to do is to copy the Bluemarine page template into our descartes/ theme directory.

$ cp drupal/themes/bluemarine/page.tpl.php \
drupal/sites/all/themes/descartes

Now we have a copy of the template that we can tailor to our needs. According to the rules of theme inheritance, this new template now overrides the Bluemarine page.tpl.php file. The theme system will use the Descartes page.tpl.php, now, and will ignore the parent Bluemarine template.

Taking a glance at the template, we can see many of the variables that we have discussed in action. Don't feel obligated to labor over the details of this template. We will be focusing on the highlighted section.

<?php
// $Id: page.tpl.php,v 1.25 2007/09/05 08:42:02 dries Exp $
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="<?php print
$language->language ?>" xml:lang="<?php print $language->language
?>">
<head>
<title><?php print $head_title ?></title>
<?php print $head ?>
<?php print $styles ?>
<?php print $scripts ?>
<script type="text/javascript"><?php /* Needed to avoid Flash of
Unstyle Content in IE */ ?> </script>
</head>
<body>
<table border="0" cellpadding="0" cellspacing="0" id="header">
<tr>
<td id="logo">
<?php if ($logo) { ?><a href="<?php print $base_path ?>"
title="<?php print t('Home') ?>"><img src="<?php print $logo
?>" alt="<?php print t('Home') ?>" /></a><?php } ?>
<?php if ($site_name) { ?><h1 class='site-name'><a href="<?php
print $base_path ?>" title="<?php print t('Home') ?>"><?php
print $site_name ?></a></h1><?php } ?>
<?php if ($site_slogan) { ?><div class='site-slogan'><?php
print $site_slogan ?></div><?php } ?>
</td>
<td id="menu">
<?php if (isset($secondary_links)) { ?><?php print
theme('links', $secondary_links, array('class' => 'links',
'id' => 'subnavlist')) ?><?php } ?>
<?php if (isset($primary_links)) { ?><?php print theme('links',
$primary_links, array('class' => 'links', 'id' =>
'navlist')) ?><?php } ?>
<?php print $search_box ?>
</td>
</tr>
<tr>
<td colspan="2"><div><?php print $header ?></div></td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" id="content"> <tr> <?php if ($left) { ?><td id="sidebar-left"> <?php print $left ?> </td><?php } ?> <td valign="top"> <?php if ($mission) { ?><div id="mission"><?php print $mission ?></div><?php } ?> <div id="main"> <?php print $breadcrumb ?> <h1 class="title"><?php print $title ?></h1> <div class="tabs"><?php print $tabs ?></div> <?php print $help ?> <?php if ($show_messages) { print $messages; } ?> <?php print $content; ?> <?php print $feed_icons; ?> </div> </td> <?php if ($right) { ?><td id="sidebar-right"> <?php print $right ?> </td><?php } ?> </tr> </table>
<div id="footer">
<?php print $footer_message ?>
<?php print $footer ?>
</div>
<?php print $closure ?>
</body>
</html>

The highlighted table in the above code provides the basic layout for the page. It is composed of three cells, all arranged in one row. Each sell corresponds to a region: left, content, and right.

Note

CSS and the left and right regions

Notice that while the left and right regions are stored in the $left and $right variables, the IDs for the corresponding containers are named sidebar-right and sidebar-left. We styled these while developing our CSS-only theme earlier.

Instead of this three-column table layout, we want only two columns: one for the right region, and the other for the content region.

The left region will reside inside a <div/> element inside of the content section, and we will modify our new.css stylesheet to move that region over to the right.

To do this, we will remove the third <td/> element that looks as follows:

<?php if ($right) { ?><td id="sidebar-right">
<?php print $right ?>
</td><?php } ?>

Note that this third column is only displayed if the $right variable is defined. We will preserve this characteristic as we move the right region into the second column of the table.

<td valign="top">
<?php if ($mission) { ?><div id="mission"><?php print $mission
?></div><?php } ?>
<div id="main">
<?php if ($right) { ?> <div id="sidebar-right"> <?php print $right ?> </div> <?php } ?>
<?php print $breadcrumb ?>
<h1 class="title"><?php print $title ?></h1>
<div class="tabs"><?php print $tabs ?></div>
<?php print $help ?>
<?php if ($show_messages) { print $messages; } ?>
<?php print $content; ?>
<?php print $feed_icons; ?>
</div>
</td>

We've added only five lines to implement our change. Now, if the $right variable is defined, the template will encapsulate the region's content inside a <div/> element with the same ID (sidebar-right) that the <td/> element uses in the Bluemarine style.

If we look at the site, we can see the result of this small change:

A Page Template for Descartes

What are the effects of this change? First, the orange mission section on the top now spans two-thirds of the screen, crossing over the right sidebar.

Glancing back at the above code, we can see why: The<div id="mission"/> element is above the<div id="sidebar-right"/> element.

The second difference is the wrapping of text. In the old template, the right-hand sidebar extended the length of the table. Now it only extends as far as it requires to display all of its content. Material in the center section (which displays the $content) can now extend to the right beneath the right-hand region.

Note

Remember, Drupal caches theme information. To see your changes, you may need to clear the cache using the Devel module.

Should we want to change the HTML code for blocks, boxes, comments, or nodes, we could use a similar strategy. Copy the template or templates from the parent theme, and modify them at will.

Using PHP to Override Theme Behavior

Sometimes there are cases where we want to perform some additional manipulation on data before passing it on to the templates.

Some such manipulation could be done inside the PHPTemplate files. After all, they are really just PHP files. However, that violates the principle of separation we have adopted; templates should simply display content, not do any processing.

Additionally, some data comes into the theme in a somewhat unpredictable way. The content of the $right variable in the page.tpl.php template, for example, is an arbitrary string. Operating on each node or block represented in that variable is difficult, as it would require sophisticated (and error-prone) parsing.

Fortunately, there is a better way. We can interoperate with the theming system at a lower level. We can use a template.php file to override theme functions.

Note

The template.php file is a construct supported by the PHPTemplate engine. Not all engines support template.php files. Some themes, like Chameleon, use a similar technique to that of the template.php file, but without using any theme engine at all. Chameleon strategically uses and overrides theme functions to create HTML.

Theme functions, the core of which are defined in drupal/includes/theme.inc, define how themes process information passed into the theme system. These functions are part of the Drupal core (and, in some cases, part of modules), and are executed before control is handed over to the theme engine.

Theme functions are similar to hooks. Just like hooks, they can be overridden. The template.php file provides PHPTemplate-based themes with the ability to override default theme behavior.

Custom Breadcrumbs

Let's look at a simple example. While creating our page.tpl.php template, we looked briefly at the $breadcrumb variable as it is passed into the page template. $breadcrumb, as passed in here, contains a string that looks something like the following code snippet:

<div class="breadcrumb">
<a href="/drupal/">Home</a> "
<a href="/drupal/?q=node/add">Create content</a>
</div>

This code is generated from the theme_breadcrumb() function in drupal/includes/theme.inc. The function in its entirety looks as follows:

/**
* Return a themed breadcrumb trail.
*
* @param $breadcrumb
* An array containing the breadcrumb links.
* @return a string containing the breadcrumb output.
*/
function theme_breadcrumb($breadcrumb) {
if (!empty($breadcrumb)) {
return '<div class="breadcrumb">'. implode(' " ', $breadcrumb) .'</div>';
}
}

The theme_breadcrumb() function gets an array of strings, each one a breadcrumb item wrapped in a link. The function then combines all the items in this array, separated by the double right angle quote ("). The whole thing is then wrapped in a<div/> and returned.

This markup is hard-coded into Drupal, and cannot be easily changed from a template.

But what if we don't like separating breadcrumbs with a double right angle quote?

The answer: We can override this theme function with a PHPTemplate function defined in a template.php file inside our Descartes theme.

For example, let's look at a method for using an image as a separator.

Here is our drupal/sites/all/themes/descartes/template.php file:

<?
// $Id$
/**
* @page
* Override theme_() functions for the Descartes theme.
*/
// Overrides theme_breadcrumb()
function phptemplate_breadcrumb($breadcrumb) {
if (empty($breadcrumb)) return;
$sep = '<div class="breadcrumb-separator">&nbsp;&nbsp;</div>';
$breadcrumb_string = '<div class="breadcrumb">'
. implode($sep, $breadcrumb) .'</div>';
return $breadcrumb_string;
}

This file follows the same coding conventions as Drupal modules. We have one function here—phptemplate_breadcrumb(). This function, which acts as part of the PHPTemplate engine, overrides the theme_breadcrumb() function we examined earlier.

Whereas the theme_breadcrumb() function separated elements with the double right angle quote, this function separates them with this string (the value of $sep):

<div class="breadcrumb-separator">&nbsp;&nbsp;</div>

A<div/> with a new breadcrumb-separator class wraps a couple of non-breaking spaces. This function will print output that looks something like the following code:

<div class="breadcrumb">
<a href="/drupal/">Home</a> <div class="breadcrumb-separator">&nbsp;&nbsp;</div>
<a href="/drupal/?q=node/add">Create content</a>
</div>

Now with an extra class declaration in new.css, we can add a small background image, which will appear "behind" the two blank spaces. It will give the appearance of a separator:

div.breadcrumb-separator {
display: inline;
background: url(orange_dot.gif) no-repeat bottom left;
}

Viewed through the browser, we now have a breadcrumb trail that looks as follows:

Custom Breadcrumbs

Our phptemplate_breadcrumb() theme function is now performing some custom pre-processing on the breadcrumb trail.

But the HTML (and CSS) produced by this method is admittedly a little clunky. Let's look at a more elegant solution, which involves interacting with another theme function.

Interacting with Other Theme Functions

A quick perusal of the contents of drupal/includes/theme.inc will give you some indication of what theming functions can do. Here are just a few to give some idea:

  • theme_image(): Format an image for display.
  • theme_links(): Format a list of links.
  • theme_progress_bar(): Display a progress bar.
  • theme_username(): Format a username for display.
  • theme_table(): Format data and display it as a table.

    Note

    These functions can be called by modules as well as by themes.

    The preferred method of calling them in the module context is theme('<hookname>', $arg1, $arg2,...), where<hookname> should be replaced by the string after the first underscore in the theme function.

Rather than calling these theme functions directly, it is better to call them through a mediating function, theme(). The theme() function takes the theme hook suffix as the first argument, and the theme hook's arguments as additional arguments. In other words, a call to theme should look something like this: theme('<hookname>', $arg1, $arg2,...). Here,<hookname> should be replaced by the string after the first underscore in the theme function. For example, invoking theme('image', 'images/socrates.png', 'Bust of Socrates') would result in some version of theme_image() being executed with the arguments'images/socrates.png' and'Bust of Socrates'.

What do we mean by "some version of theme_image()"? The answer to this question explains why it is better to use theme() than to call a theme_<hookname>() function itself.

As we have seen in the previous example, it is possible to override a theme function. Invoking theme('images', $a, $b) causes the theme system to check for a function or template that overrides theme_image(). If one is found, that function or template is used instead.

In short, theme() makes use of the full theme system, while calling theme_<hookname>() function directly may cause your application to ignore the current theme configuration and use only the default.

Note

Templates can override theme hooks

A theme_<hookname>() function can also be overridden with a PHPTemplate template file. For example, to override theme_image(), we could create a template named image.tpl.php. The variables normally available as arguments will be accessible here as variables according to the method signature for theme_image().

With that explanation, we are ready to move on to our revised breadcrumb implementation.

One way to represent our breadcrumb trail is as a list. Lists are easier to style, and we have a nice built-in theme function for taking an array and generating a list: theme_item_list().

The theme_item_list() function takes up to four arguments, though only the first is required:

  1. Array of list items
  2. Title of list (optional, default: null)
  3. List type, ul for unordered, ol for ordered
  4. An associative array of attributes to add to the list element (the<ol/> or<ul/> elements)

Of course, we don't want to call theme_item_list() directly. Instead, we will access the hook via the theme() function.

Re-working our earlier example, we can create a phptemplate_breadcrumb() function that looks as follows:

// Overrides theme_breadcrumb()
function phptemplate_breadcrumb($breadcrumb) {
if (empty($breadcrumb)) return;
$attr = array(
'class' => 'breadcrumb-items'
);
$crumbs = theme('item_list', $breadcrumb, null, 'ul', $attr);
return '<div class="breadcrumb">' . $crumbs . '</div>';
}

This function now makes use of item_list to create an unordered list of breadcrumb items with the style class name set to breadcrumb-items.

Since we still want existing style information to apply where appropriate, we wrap the new list in a<div> with the breadcrumb class.

We can now make use of our newly-defined breadcrumb-items class in our stylesheet:

ul.breadcrumb-items li {
list-style-image: url(orange_dot.gif);
float: left;
margin-right: 1em;
margin-left: 1em;
}
ul.breadcrumb-items li.first {
margin-left: 0px;
list-style: none;
}

These first definition instructs the browser to display the list as a single row of links, using the orange_dot.gif image (the same one we used before) instead of a bullet.

But we don't want the first item to display a bullet. Nor do we want a blank space on the left. So we create a second definition that applies only to the first item in the list.

The HTML output is now a little more elegant than the previous version:

<div class="breadcrumb">
<div class="item-list">
<ul class="breadcrumb-items">
<li class="first"><a href="/drupal/">Home</a></li>
<li class="last"><a href="/drupal/?q=node/add">Create
content</a></li>
</ul>
</div>
</div>

Notice that the theme_item_list() function wrapped the returned list in a<div/> with the item-list class. This makes it easier to apply global styles to all lists rendered through the theme.

What does this look like when rendered by the browser? It should look something like this:

Interacting with Other Theme Functions

Since the HTML is now based on lists, we can now do any fine tuning of the appearance in pure CSS.

The template.php file provides a convenient way to override the default theme behavior. It also provides access to data before the data is handed on to the template. Accessing such data before it is themed is called preprocessing. For example, you can make additional variables accessible to the page.tpl.php template by overriding the theme_preprocess_page() hook. See the API documentation for details: http://api.drupal.org/api/function/template_preprocess_page/6.

template.php Gotchas

When using a template.php file in PHPTemplate themes, there are a couple of things you might want to watch out for.

First, you can only override a theme function once. While this seems simple, there is one circumstance in which this may be surprising.

While you can declare a template.php file in a sub-theme even if there is a template.php in the parent, you cannot override functions declared in the parent. If the parent's template.php declares phptemplate_breadcrumb(), then the sub-theme's template.php cannot declare phptemplate_breadcrumb(). Doing so will result in a PHP errors for redeclaring a function.

Second, we have seen that theme hooks (theme_<hookname>()) can be overridden by PHPTemplate templates and also by functions in the template.php file. But what happens if you declare both?

The phptemplate_<hookname>() function overrides the theme function that would otherwise load the template. Therefore, if phptemplate_block() is defined in template.php and there is a template named block.tpl.php, the phptemplate_block() function will be called to render all blocks, and the block.tpl.php file will never be used.

If you need to manipulate some content before it is passed to a template, don't do this by defining phptemplate_<hookname>(). Instead, use one of the preprocessor functions, such as phptemplate_preprocess_block(), phptemplate_preprocess_node(), or phptemplate_preprocess_page().

At this point, we've created a new theme. We've added custom CSS, images, PHPTemplate files, and even some custom theme hooks... what else does it take to complete a theme?

One more thing.

Creating a Screenshot

A complete theme ought to have a screenshot that can be displayed in the theme manager in Administer | Site building | Themes.

Once the theme is completed, we can generate a screenshot, scale it to the requisite 150x90 pixel dimensions, and then save it as a PNG image named screenshot.png.

All we need to do now is copy screenshot.png into our theme directory and Drupal will automatically display it as necessary.

If for some reason you want to name the screenshot something else, you can set the screenshot directive in the theme's .info file: screenshot = image.png.

That's it. We're done creating our new theme.

Of course, this theme is a derivative theme. What does it take to go on and build a top level theme—one with no parent?

From Here to a Full Theme

We have been looking at sub-themes. But technically speaking, it is not that difficult to move from here to a top-level theme. Since theming is not the focus of the book, we will just glance at what it takes to create a full theme.

If the theme is based on PHPTemplates, the process of creating a top-level theme doesn't differ much from what we have done already. You will need to make sure to do a few things. Here is a short checklist:

  • Create a theme .info file that doesn't set the base theme to some other theme. Instead of using the base theme directive, you should use the engine directive: engine = phptemplate.
  • Create all the major templates: page.tpl.php, node.tpl.php, block.tpl.php, and comment.tpl.php. Of course, you can create others, as well.
  • Add a logo.png image. Of course, you can use one of the standard Druplicon images as a starting point (http://drupal.org/node/9068).
  • Create a style.css file, as well as any other CSS files the theme needs. The style-rtl.css ("right to left") and print.css stylesheets are good to include.
  • Add a screenshots.png image with a 150x90 screenshot of your theme.

The included Pushbutton and Bluemarine themes are good guides for getting started. In short, though, it is a small step from creating derivative themes to creating top-level themes.

More advanced PHPTemplate themes may also make use of the template.php preprocessing page, additional JavaScript files, and other rich features. Garland is an example of a complex theme.

For more information on developing themes, including links to the relevant APIs and auxiliary documents, the official Drupal theme tutorial is the best place to go: http://drupal.org/node/165706.