A graphical progress bar widget for FormsAPI

Submitted by Frederic Marand on

A graphical progress bar widget for Forms APIIn the previous post, we saw how to create a XHTML progress bar widget for Forms API, using theme_progress_bar. The next logical step is now to create a graphical equivalent to that progress bar, as an example for far more advanced fully graphical widgets made possible using a similar mechanism.

Of course, since the point is about the use of FAPI theming, this example will only demonstrate a basic graphic widget, but I'm sure you can come up with far more impressive widgets. let me know if you have come up with something better looking.

The question

Much as in the previous case, we would like to create a widget which could be used like this:

<?php
/**
 * Sample form using the graphical progress bar widget
 *
 * @return array
 */
function progress_bar_form()
  {
 
$form = array();
 
 
$form[] = array
    (
   
'#type'        => 'fapi_progress_bar_png',
   
'#title'       => 'FAPI Progress bar in PNG Format',
   
'#description' => 'A FormsAPI progress bar widget using a PNG image',
   
'#percent'     => rand(0, 100),
    );

  return
$form;
  }
?>

The solution

Extrapolating from the text version

This is a mostly obvious extrapolation of the example we saw in the previous post : instead of formatting our widget around this line:

<?php
$ret
= theme_progress_bar($element['#value'], $element['#title']);
?>

... we will now be using an image, generated on the fly, like:

<?php
$url
= url('progress_bar/progress_bar/' . $element['#percent']);
$percent = $element['#percent'] . '%';
$ret = "<img src= "$url" alt="$percent" /" . '>';
?>

This points to the need for an image-generating function, and a menu callback to invoke it.

Generating the image

Drupal is normally used on systems with the GD2 graphics library, which we can use to create a simple-minded progress bar:

<?php
/**
 * Output a progress bar as a PNG image
 *
 * It displays the percent ratio within the progress bar itself.
 *
 * @param int $usValue
 * @return void
 */
function progress_bar_view($usValue = 0)
  {
 
$width = 400;
 
$height = 16;
 
$range = 100;
 
  if (!
is_numeric($usValue) || $usValue < 0)
    {
   
$value = 0;
    }
  elseif (
$usValue > 100) // we know it's a number
   
{
   
$value = 100;
    }
  else
    {
   
$value = $usValue; // sanitized above
   
}
?>

The first step we need to take is checking the input, which is an unsafe string, coming from the outside. The fragment of code above will only accept numeric values in the [0, 100] range and convert anything else to an acceptable value within that range.

For the second step, we specify in the reponse headers that the content which will be output is not the default text/html produced by most PHP scripts but an image in PNG format:

<?php
  drupal_set_header
('Content-type: image/png');
?>

After this, we can create an in-memory image buffer, by allocating space for it in true color format, and allocating colormap entries for the color we will be using.

<?php
  $image
= imagecreatetruecolor($width, $height);
 
$colorSilver = imagecolorallocate($image, 192, 192, 192);
 
$colorBlue   = imagecolorallocate($image, 0x23, 0x85, 0xc2); // garland "blue lagoon" top
 
$colorWhite  = imagecolorallocate($image, 255, 255, 255);
?>

And we can then draw with the GD primitives, in this example a blue frame, white background, and silver progress bar.

<?php
  $fontSize
= 5;
 
imagerectangle($image, 0, 0, $width - 1, $height - 1, $colorBlue);
 
imagefilledrectangle($image, 1, 1, $width - 2, $height - 2, $colorWhite);
 
imagefilledrectangle($image, 1, 1, ($width - 2) * $value/ $range, $height - 2, $colorSilver);
?>

Adding text requires a bit of positioning, based on the rectangle size.

<?php
 
for ($i = 1 ; $i < 10 ; $i++)
    {
   
$xOff = $i * $width / 10 ;
   
imageline($image, $xOff, $height, $xOff, $height * 9 / 10, $colorBlue);
    }
 
//  imageline($image, 0, 0, $width, $height, $colorBlue);
//  imageline($image, 0, $height, $width, 0, $colorGreen);
 
$caption = sprintf("%5.2f%%", 100.0 * $value / $range);
 
$captionWidth = imagefontwidth($fontSize) * strlen($caption);
 
$captionHeight = imagefontheight($fontSize);
 
imagestring($image, $fontSize, ($width - $captionWidth) / 2, ($height - $captionHeight) / 2, $caption, $colorBlue);
?>

At this point the image is ready in the buffer, and we can ask GD to output it, and release the buffer.

<?php
  imagepng
($image);
 
imagedestroy($image);
?>

Now, since GD already output the image file, we do not want Drupal to output anything, so we have to return NULL.

<?php
 
return NULL;
  }
?>

Putting it all together gives this function:

<?php
/**
 * Output a progress bar
 *
 * It displays the value/range ratio on top of the progress bar itself.
 *
 * @param int $usValue
 * @return void
 */
function progress_bar_view($usValue = 0)
  {
 
$width = 400;
 
$height = 16;
 
$range = 100;
 
  if (!
is_numeric($usValue) || $usValue < 0)
    {
   
$value = 0;
    }
  elseif (
$usValue > 100) // we know it's a number
   
{
   
$value = 100;
    }
  else
    {
   
$value = $usValue; // sanitized above
   
}
 
 
drupal_set_header('Content-type: image/png');
 
 
$image = imagecreatetruecolor($width, $height);
 
$colorSilver = imagecolorallocate($image, 192, 192, 192);
 
$colorBlue   = imagecolorallocate($image, 0x23, 0x85, 0xc2); // garland "blue lagoon" top
 
$colorWhite  = imagecolorallocate($image, 255, 255, 255);
 
 
$fontSize = 5;
 
imagerectangle($image, 0, 0, $width - 1, $height - 1, $colorBlue);
 
imagefilledrectangle($image, 1, 1, $width - 2, $height - 2, $colorWhite);
 
imagefilledrectangle($image, 1, 1, ($width - 2) * $value/ $range, $height - 2, $colorSilver);
  for (
$i = 1 ; $i < 10 ; $i++)
    {
   
$xOff = $i * $width / 10 ;
   
imageline($image, $xOff, $height, $xOff, $height * 9 / 10, $colorBlue);
    }
 
//  imageline($image, 0, 0, $width, $height, $colorBlue);
//  imageline($image, 0, $height, $width, 0, $colorGreen);
 
$caption = sprintf("%5.2f%%", 100.0 * $value / $range);
 
$captionWidth = imagefontwidth($fontSize) * strlen($caption);
 
$captionHeight = imagefontheight($fontSize);
 
imagestring($image, $fontSize, ($width - $captionWidth) / 2, ($height - $captionHeight) / 2, $caption, $colorBlue);
 
imagepng($image);
 
imagedestroy($image);
  return
NULL;
  }
?>

Making the image available

Now we need to tell Drupal to invoke this function on the path we are using in the theme function. This is the purpose of the hook_menu implementations. In Drupal 5, for instance:

<?php
function progress_bar_menu($may_cache)
  {
 
$viewAccess = user_access('access content');

 
$items = array();
  if (
$may_cache)
    {
   
$items[] = array
      (
     
'path'               => 'progress_bar/progress_bar',
     
'callback'           => 'progress_bar_view',
     
'access'             => $viewAccess,
     
'title'              => t('Widget : full screen'),
     
'type'               => MENU_CALLBACK,
      );
    }
  return
$items;
  }
?>

Take care if using Drupal 6 or 7: the menu syntax has changed ! But in all cases, the 'type' => MENU_CALLBACK is what specifies Drupal to handle the path but not create an entry in the site menus.

Going further

Like the previous text-based example, this is only a demo, to show how you can create graphical read-only widgets for FormsAPI, à la markup, without allowing them to have children or having a modifiable value. Nothing prevents you from elaborating on the example to create your own widgets, possibly supporting children, unlike this demo.

Also, for better readability, this example does not apply the Drupal coding standards, but your actual code implementing such a widget should typically apply them.

Beyond the unglamorous look of our new progress bar, this method still has a small problem and could be made significantly better with a fairly small change, especially on sites with high frequentation. Can you see how ? Then read on.