The FormFactory
by
Marc Van den Berghen (VdBSoft@web.de)
Suddently, it was my turn to shout
victory: I had just
found
a message from David Stone entitled “A whole World of fun forms”
in which he presented on a silver
plate, the solution for my “button problem”:
the SetWindowRgn API-Call!
With the help of this function, one can limit the visible area of a
window to a so-called “Windows region”. The
shape of this region is not restricted in any kind (it can even represent
holes).
The
region has to be defined with one or more Windows-based
shapes (rectangles and ovals) and that API-Call assigns the region to a window,
which
then takes the shape of that region.
From the Microsoft On-Line help: “In Microsoft® Windows®, a
region is a rectangle, polygon or ellipse (or a combination of two
or more of
these shapes) that can be filled, painted, inverted, framed and used
to perform hit testing (testing for the cursor location).”
That same evening, I built a small demo form for Delphicus, with a real
elliptic button (Ebutton.wfm).
There was no remaining rectangular part of the button: it was a real
oval button.
Thanks to David Stone, that was exactly what was needed to make
that Denmark vacation fruitful for me.
The dBKon 2002
As you may imagine, Delphicus was not
satisfied. He grumbled about the ragged edges of the button but had to
admit after a closer look at the
Delphi application, that the edges there were more or less the same.
dBASE honour was saved. Moreover, I had a neat little
subject for a speech I was going to hold at the German dBASE Conference
in 2002.
During the conference, Niels Bartels saw the form in triangular-shaped
form that I used as an example (see Triangle.wfm)
and he decided to make something similar for one of his customers:
a form having the shape of
a motorcycle. I told him that it would be a huge undertaking
to try to build a detailed motorcycle out of lines, rectangles and
circles. Nevertheless one year later, he showed us his form at the
dBKon2003:
it looked tremendous!
At that time, Jonny Kwekkeboom and I were working on a dBASE
jukebox, a player in the style of the Windows media player.
After seeing Niels’ work, we wanted to do something similar.
The Windows OLH states that it is possible to combine simple regions to
obtain a result of astonishing complexity (as with the motorcycle).
The drawback is that the shapes from which you have to start are
very primitive. The amount of work to construct a region rises
exponentially with its complexity. Even the shape we had chosen for
the music player took a considerable amount of time. For each little
change to the silhouette, the same time-consuming work to create
a region had to be done anew.
It was clear: we needed an automated solution!
Jonny designed the bitmap that represented the music player. Our idea was to
write a routine that would analyze the bitmap
and create the Windows-Region automatically out of the bitmap.
Out came this article and the FormFactory Class which does exactly
what we had hoped for.
The solution
A possible solution popped
up because Jonny’s bitmap had a small blue
border that shouldn’t be seen. I thought that we could
scan the bitmap, pixel by pixel, and add the pixels delimited with
Jonny’s blue border to the region. This means that the
shape would be constructed out of the smallest
possible
base
shape: one-pixel building blocks (which are merely
rectangles having a one-pixel height and width). To make the class
more user-friendly, we decided that one should be able to
choose the colour of the bitmaps
that don’t belong to the region. I call it the transparency color.
With the help of the API-Function GetPixel( ),
it was a piece of cake to extract the colour of a given pixel and to compare
it to the
transparency colour. The result was the pseudo-code below:
for y = 0 to height_of_bitmap - 1 |
|
for x = 0 to width_of_bitmap -1 | |
Pixelcolor = GetPixel( image_Handle, x, y ) | |
if Pixelcolor # transparency_color | |
add_to_region( ) | |
endif | |
endfor | |
endfor | |
The first goal was met: we were able to build a region out of any
image, even the most detailed ones. Just designate the transparency
colour and the class will do the rest.
Unfortunately a new problem arose; Microsoft simply forgot to implement
a way to store an existing region to the disk. Each time the
form was opened, the shape had to be calculated anew. With small bitmaps, the
delay was still acceptable. However, the greater the form — and
so the bitmap — the longer it took to create the region on the fly. The
only chance to reduce the start-up time was to create a simpler representation
of
the
region, store it to the hard disk and load it from there.
The trick was to group an horizontal strip of N adjacent pixels to a “rectangle” (as
you remember, a rectangle is one of the possible base shades), to add
a few escape codes to mark the beginning of a new line and the starting
position of the new horizontal “rectangle” (atually a new line).
The outgoing sequence of numbers is then stored in a .RGN file.
Of course we need an algorithm that reads the numbers out of
the file
and transforms them back into a number of rectangles having a
one-pixel height that, finally, have to be combined to represent the
wanted shape.
After some tests, it was clear that even with large and complicated
bitmaps, the delay during start-up was minimal. The final goal
was achieved. The design of our player could be adapted
easily to Jonny’s
bitmap.
The Code
Before creating a non-rectangular
form, I want to take a deeper (coding) look at the class behind the scene.
If you want to see the
form-building class in action, you can jump directly to
the next section.
There is a special characteristic of the image on the form: it is
sizeable!
Usually, controls on a form are fixed; they keep their width
and length. However, from time to time, it could be handy for the controls
to behave
otherwise; more precisely, that they have a border (like that of a sizeable
window) which enables the
user to shrink or expand that control with the mouse. Since
every control on a form is nothing but a window (technically speaking), most of the API possibilities for a form also exist
for the controls:
you just have to make them available. In this case, the
following lines tell the OS to make the image control sizeable.
local zw |
|
zw = getwindowlonga( this.image1.hwnd, -16 ) // GWL_STYLE | |
zw = bitor(zw,0x00040000) // WS_THICKFRAME | |
setwindowlonga( this.image1.hwnd, -16, zw ) | |
Beside the two constants — GWL_STYLE and WS_THICKFRAME
— we only need the handle of the control that we
want to make resizeable. If you try the same
code with other controls, you will see that it works with
all visual objects that have an hwnd property
(almost all dBASE controls). One possible application for this could be a
custom editor, allowing the user to size it at will (see Editor_Resize.wfm).
Let’s have a closer look at the code behind the
region-creation process and give life to the pseudo-code
above.
The first step is to get rid of the unnecessary
color information. The only thing we need to know is
whether a pixel should be transparent or not. So we create
a two-dimensional array that has the same size as the image
in our form. Next, we fill this array with zeros and ones,
the value for a particular pixel determined by whether it should be transparent
or not (the
returned value
of the GetPixel() API-Function).
In a certain way, the array is a monochrome representation of our image,
where the positions we want to be transparent are black
and everything else is white.
The next step is to improve the speed of the form-opening process.
Also, we must find a way to easily store that data
to the harddisk. As we’ve seen, our solution was to build
rectangles with an height of 1 pixel. Each of these
rectangles can be represented with two numeric values: its
leftmost x-position and
its width. Imagine the following sequence of numbers
in our monochrome array:
00000111111100111111110000000011 |
|
If this is the first line in our image,
we can translate it to the
following sequence of numbers: 6,7,15,8,31,2,1,0.
These numbers mean
that the first line begins at position 6 with a 7-pixel-long rectangle, followed
at position 15 with an 8-pixel rectangle, followed
at position 31 with a 2-pixel rectangle. The last two
numbers at the end of the sequence (here in red) represent
an escape sequence that tells us that the line is terminated
and that everything
that
comes next
belongs
to the next line of our region.
In this way, we have
changed the representation of our region from 32 individual pixels
to
3 rectangles, reducing the necessary work to
build the region
by a factor of about ten. At the same time we found
a way to store the region since the above numbers can be saved into a file.
How do we read that file? Give a look at WinRgn.cc:
it defines a custom class called Windowrgn() to
which the name of the file is passed as a parameter. The clip() method
of that class reads
the values stored in the file and
then builds the region.
Finally, there is one more thing that deserves a closer
look: the move() method
in WinRgn.cc. In
order to be moveable, a normal form must have a caption
bar. If a region doesn’t show at least
a part of the caption bar of the form,
we lose the ability to drag that form
around. We also lose the
minimize and close buttons.
To overcome these shortcomings, we could write a combination
of mouseDown and mouseMove event handlers. However,
a much
more
elegant way is to let the OS do all the work.
If you look at the move() method,
you will find a line with a SendMessage() API-Call.
This API-Call simulates a click of the mouse
in the caption bar: the OS thinks that we
want to drag the form and does just that for
us. The interesting thing
is that the function needs only
a valid window handle: it isn’t
even necessary that the window has a caption
bar. This gives us the ability to
move all controls in a form
without further programming work. If a user wants
to arrange controls individually, this can be
achieved with
just this
API-Call (see MoveCtrl.wfm)
See
the FormFactory in Action
The FormFactory code
package (available through a link at the end of this article)
consists of two parts. One is responsible for the creation
of the region, the other is responsible
for hiding everything but that region on a form.
Form.rgn1 = new WindowRgn("YourImage.rgn", Form) |
|
or:
Form.rgn2 = new WindowRgn("AnotherOne.rgn", Form.pushbutton1) |
|
In the first example, the region “YourImage” will be applied on the form, while in the second example, the region called “AnotherOne” will be used on a pushbutton. In both cases, the shape of the object (Form or pushbutton or whatever window) will no longer be rectangular, but will fit the shape of the region that was used. To do the clipping, you simply call the clip() method:
Form.rgn1.clip( ) |
|
If
you want not only the shape but also the content of your
region image, you’ll have to assign
the original .rgn file
to the background property
of the form.
When the form is closed, you should properly release the regions.
This is done with the cleanup() method.
There is one supplemental method that is handy
if you clip a form. Restricting the client-area
to your
shape
of choice
means
that you
don’t have a caption bar anymore: so you
can’t move the form around. To solve this
problem, use the move() method.
Just assign
it to the
onLeftMouseDown event handler of the form to
be able to move the form around without caption
bar:
Form.onLeftMouseDown = {; form.rgn1.move() } |
|
Now let’s do a simple exercise. Download and unzip the code available at the end of this article. Under dBASE, run the form RegionBuilder.wfm. Click the Load Image toolButton and select dBCon2004.jpg — that’s the image on the mouse pad that was given by Ronnie MacGregor to every attendee at dBCon2004 in Montréal. Resize the Image Viewer (and the form, if needed) until it’s exactly the same size as the image. Click on the Create Region toolButton and select a white pixel around the maple leaf. Wait until a messageBox tells you that the region has been created. Close the form. Load dBCon2004.wfm and move it around. Finally, push the Close button near the top right corner of the form. Voilà.
Only Forms ?
In Windows, every visual
object is a window. The FormFactory class only needs a valid
handle to
act
on any given control.
For example, if you load Toolbar.wfm,
you’ll see a rather non-conformist
toolBar that has the shape of a stylised
atom.
With the mouse, it can be dragged around like a floating
toolbar: you can even
place it on top of the grid. So you might
create wild objects like a grid with
a hole in it, a pushbutton that looks like
a spider for a SpiderMan-fan among your friends. The
outcome of your graphic interfaces is thus only limited by your imagination
;-)