Win32::GUI Tutorial - Part 1 - Our first Win32::GUI Program

Hello, GUI world

OK, let's begin. The traditional "first program" for any programming environment prints "Hello, world", so let's go with that.

To start with, we load Win32::GUI.

use Win32::GUI;

We are working in a windowing environment, so we need a window. Main windows are created using Win32::GUI::Window->new(). The parameters to new() are a list of key => value pairs, which define the properties of the window. For our simple example, all we need are a width and a height (if we don't specify the window size, the default is to make the window as small as possible - so there is no space for the text!).

$main = Win32::GUI::Window->new(-width => 100, -height => 100);

We save the window in the variable $main so that we can refer to it later.

Now, what about the text? Text can be put into a window using a "label" control. To add a label to a window, use the AddLabel() method. AddLabel() takes a list of options, similar to the new() call above. In this case, the only option we need is -text, which specifies the text to display. By default, the label's size is just big enough to fit its text, and its position is in the top left of its containing window. This is fine for us.

$main->AddLabel(-text => "Hello, world");

OK, now we need make sure the window will be displayed. By default, windows start hidden, so they won't be visible on screen. To make them visible, we need to use the Show() method.

$main->Show();

Finally, we need to start up a Windows "message loop". This shows the application windows, and waits for user interaction. The Win32::GUI message loop is implemented in the Win32::GUI::Dialog() function. All that is needed is to call that function, and wait for it to return.

Win32::GUI::Dialog();

One thing remains. At the moment, we are displaying the window, and waiting for user interaction. However, we haven't said what to do if any user interaction (called an event) occurs! Worse still, we haven't even said what to do when the window closes, so the message loop keeps on spinning, even after the window closes - so we never return from Win32::GUI::Dialog...

So we need to react to events. How do we do that? Well, the first thing we need is for any window or control which is to react to events to have a name. This name is specified using the -name option. So we change the statement which creates our main window as follows

    $main = Win32::GUI::Window->new(
		-name   => 'Main',
		-width  => 100,
		-height => 100,
	);

Now, we define event handlers for the window. Event handlers are simply Perl subroutines with specific names - <window name>_<event name>. For example, the event which happens when the window is closed is the Terminate event, so the event handler we need for our main window is Main_Terminate.

Event handlers should return one of three specific values:

  • 1: Proceed, taking the default action defined for the event.

  • 0: Proceed, but do not take the default action.

  • -1: Terminate the message loop.

Obviously, what we want for the Main_Terminate event is to return -1 (terminate the message loop). So, we have

sub Main_Terminate {
    -1;
}

More generally, we could do some processing before returning from the event handler, but we don't need to here.

So, let's put it all together.

    use Win32::GUI;
    $main = Win32::GUI::Window->new(
		-name   => 'Main',
		-width  => 100,
		-height => 100,
	);
    $main->AddLabel(-text => "Hello, world");
    $main->Show();
    Win32::GUI::Dialog();

    sub Main_Terminate {
        -1;
    }

Put that in a file (say, hello.plx) and run it using perl hello.plx.


Notice how you can resize and move the window, maximize and minimize it, just like any other application window.

So that's it. Ten lines of code to produce a fully working Windows application.

Some extra touches

Now, we'll add some simple improvements to make our application look more polished.

You will notice that the application window has no title. To include a title, all we need is to add a -text option to the main window.

    $main = Win32::GUI::Window->new(
		-name   => 'Main',
		-width  => 100,
		-height => 100,
		-text   => 'Perl',
	);

Now, suppose we want the window to be just big enough to hold the label, rather than being a fixed size. This is a bit more complicated, because we need to take note of the difference between the total window size, and the client area, which is the area of the window excluding the title bar, the system, minimize, maximize, and close icons, and the window border. In other words, it is the area in which we can actually display information.

We can get the window size using the Height() and Width() methods, and we can get the client area size using the ScaleHeight() and ScaleWidth() methods.

We can get the label size similarly, using Height() and Width() (labels do not have borders, so the label area and its client area are the same).

To get the dimensions of the non-client area of the main window, we just need

$ncw = $main->Width() - $main->ScaleWidth();
$nch = $main->Height() - $main->ScaleHeight();

Now, we get the required size as

$w = $label->Width() + $ncw;
$h = $label->Height() + $nch;

As we are getting properties of the label, we should save the return value of the AddLabel() call - which is a reference to the label - in a variable $label.

And we set the main window's size using Resize()

$main->Resize($w, $h);

We have to resize after we have created the window, because we need to create the window first in order to calculate the non-client dimensions. But we do it before we show the window, to avoid any flicker.

OK, let's put all this together. We'll add the ability to specify the label text on the command line, just to be fancy (it also shows that the values of options can be set using variables, not just constant values).

use Win32::GUI;

$text = defined($ARGV[0]) ? $ARGV[0] : "Hello, world";

$main = Win32::GUI::Window->new(-name => 'Main', -text => 'Perl');
$label = $main->AddLabel(-text => $text);

$ncw = $main->Width()  - $main->ScaleWidth();
$nch = $main->Height() - $main->ScaleHeight();
$w = $label->Width()  + $ncw;
$h = $label->Height() + $nch;

$main->Resize($w, $h);
$main->Show();
Win32::GUI::Dialog();

sub Main_Terminate {
    -1;
}

Run this version and see the results. Note that if you supply a short string, the window will not shrink beyond a minimum size sufficient to show the window icons.


Changing the appearance of the text

Now, suppose we want to change the colour in which the text is displayed. This is easy. You just specify the colour as the value of the -foreground attribute of the label. Colours can be specified either as hex values in the format 0xBBGGRR, or as list references in the format [ R, G, B ].

So, to make the text red, use -foreground => 0x0000FF or -foreground => [ 255, 0, 0 ].

To change the font, you need to specify the -font attribute. But in this case, the value of the attribute is a Win32::GUI::Font object. To create a font object, use

$font = Win32::GUI::Font->new(...);

As usual, the parameters to new() are a set of options. The most important ones are

  • -size

    The font size in points.

  • -name

    The font name, such as "Times New Roman", "Arial", "Verdana" or "Comic Sans MS".

  • -bold

    One for bold, zero (the default) for non-bold.

  • -italic

    One for italic, zero (the default) for non-italic.

So, to change the format of our label, all we need is

    $font = Win32::GUI::Font->new(
		-name => "Comic Sans MS", 
		-size => 24,
	);
    $label = $main->AddLabel(
		-text       => $text,
		-font       => $font,
		-foreground => [255, 0, 0],
	);

Simple, isn't it?

Centring the window and the text

To centre the main window on the screen, we need to know the screen size. We get this using the desktop window. You can get the handle of the desktop window using Win32::GUI::GetDesktopWindow(). One point to note is that window handles are not Win32::GUI::Window objects, so they cannot be used to call Win32::GUI methods like Height(). However, these methods are overloaded so that they can be called directly (as Win32::GUI::Height()) with a window handle as an extra first parameter. See the code below for an example.

The rest of the work is just arithmetic. To reposition the main window, use the Move() method.

# Assume we have the main window size in ($w, $h) as before
$desk = Win32::GUI::GetDesktopWindow();
$dw = Win32::GUI::Width($desk);
$dh = Win32::GUI::Height($desk);
$x = ($dw - $w) / 2;
$y = ($dh - $h) / 2;
$main->Move($x, $y);

Now, we will look at centring the label in the window (you will notice that if you increase the size of the window, the text stays at the top left of the window).

The process is similar to the calculations for centring the window above. There are two main differences. First of all, the label needs to be recentred every time the window changes size - so we need to do the calculations in a Resize event handler. The second difference (which in theory applies to the window as well, but which we ignored above), is that we need to watch out for the case where the window is too small to contain the label. Just to make things interesting, we'll stop the main window getting that small, by resizing it in the event handler if that is about to happen. As the width could be too small while the height is OK, we have to reset the width and height individually. We can do this using variations on the Width() and Height() methods, with a single parameter which specifies the value to set.

OK, let's just do it.

sub Main_Resize {
    my $w = $main->Width();
    my $h = $main->Height();
    my $lw = $label->Width();
    my $lh = $label->Height();
    if ($lw > $w) {
        $main->Width($lw) + $ncw; # Remember the non-client width!
    }
    else {
        $label->Left(($w - $lw) / 2);
    }
    if ($lh > $h) {
        $main->Height($lh) + $nch; # Remember the non-client height!
    }
    else {
        $label->Top(($h - $lh) / 2);
    }
}

This does not work in build 340 of Win32::GUI, as there is a bug in the Left() and Top() methods for client windows (such as the label used here). Hopefully, this bug will be fixed in the next version.

Note that co-ordinates are calculated from the top left of the enclosing window.

Restricting the minimum size of the window

Suppose we wanto to ensure that our main window never gets smaller than 100x100 pixels. We could include code in a resize event, like we did in the previous section to make sure the window never got smaller than the label. However, this can result in fairly bad flickering. A better solution is to use the -minsize option.

    $main = Win32::GUI::Window->new(
		-name => 'Main',
		-text => 'Perl',
		-minsize => [100, 100],
	);

Simple! But in the previous example, we wanted to restrict the window size to the size of the label. We can't do this when we create the window, because we haven't created the label yet. But we can still do this - we just have to set the window's -minsize option after the label has been created. We do this with the Change() method. We just specify this before resizing the window, in our initial setup.

$main->Change(-minsize => [$w, $h]);

Putting it all together

OK, we now have a nice, fully working GUI application. Here it is, in all its glory.

    use Win32::GUI;

    $text = defined($ARGV[0]) ? $ARGV[0] : "Hello, world";

    $main = Win32::GUI::Window->new(
		-name => 'Main',
		-text => 'Perl',
	);
    $font = Win32::GUI::Font->new(
		-name => "Comic Sans MS", 
		-size => 24,
	);
    $label = $main->AddLabel(
		-text => $text,
		-font => $font,
		-foreground => [255, 0, 0],
	);

    $ncw = $main->Width() -  $main->ScaleWidth();
    $nch = $main->Height() - $main->ScaleHeight();
    $w = $label->Width()  + $ncw;
    $h = $label->Height() + $nch;

    $desk = Win32::GUI::GetDesktopWindow();
    $dw = Win32::GUI::Width($desk);
    $dh = Win32::GUI::Height($desk);
    $x = ($dw - $w) / 2;
    $y = ($dh - $h) / 2;

    $main->Change(-minsize => [$w, $h]);
    $main->Move($x, $y);
    $main->Show();

    Win32::GUI::Dialog();

    sub Main_Terminate {
	-1;
    }

    sub Main_Resize {
        my $w = $main->ScaleWidth();
        my $h = $main->ScaleHeight();
        my $lw = $label->Width();
        my $lh = $label->Height();
		$label->Left(int(($w - $lw) / 2));
		$label->Top(int(($h - $lh) / 2));
    }

That's it for this tutorial. In part 2, we will start looking at some other controls.