Looks like a Slider - Acts Like a Slider - Tastes Like Chicken

by Jeff Slarve

Published 1997-07-01    Printer-friendly version

Download the code here

Clarion for Windows (CW) is a fantastic programming language. It, along with the multitude of things possible in the Windows API and numerous add-on tools available, gives a programmer more power than I had ever dreamed possible back in my DOS days (which wasn’t too long ago). There are so many things that can be done with CW, that if I find that I need a feature not inherently built-in to it, a solution is more than likely within my grasp. Sometimes the solution is available as a third party add-on, and sometimes it needs to be written from scratch, but there is almost always a viable solution.

Often, some creative thinking and resourcefulness is what is required to get the job done. The Wright Brothers used a tomato can to fashion a carburetor for their airplane prototype. After some trial and error and perseverance, they had an airplane engine capable of powering the first flight. After they removed the tomatoes from the can, there was a marked improvement in the performance.

This same resourcefulness applies to programming. You take a look at the desired result, take a look at your chest of tools, and depending on the estimated amount of time (and patience!) it will take, roll up your sleeves and get to work on that (figurative) tomato can.

I had heard of some CW programmers looking for a slider control for their programs. In the past, there had been some indicator that Topspeed was going to put a slider control into CW, but they decided to hold off on it for now. Without the "native" CW slider control, the only other options were some 3rd party VBX’s and OCX’s. Personally, an OCX or VBX needs to provide a significant benefit before I will consider using it. The added hassle of having to register the OCX and the extra overhead in time and resources can be quite significant. This looks like a job for our pal, the tomato can.

For those that don’t know what a slider control is, it is a horizontal or vertical "track" along which a "knob" slides. One end of the track represents the lowest possible value of a variable, and the other side represents the highest possible value. In this article, we are going to design a simple slider control using only the tools available from within CW. We will use the Clarion Template Language, some OOP, and some existing controls available from with CW to create our own "slider control".

Disclaimer 1

Please keep in mind that this is just one programmer’s idea of how a slider could be built. There are many different approaches that could be made. Some could be better, some could be not as good. Most likely, this article will spark ideas from others who will carry the slider torch to new heights, and etch their names into CW history, enjoying the new taste of newly found charisma… Well, at least they might have fun writing it.

Disclaimer 2

Also, although I am making an effort to improve my OO (object oriented) programming, there is stuff in my OOP that will be considered "unconventional". In other words, don’t use this code as a tool from which to learn OOP.

Step 1 - Determine what this slider is supposed to do.

These are a few things that I had noticed when using other slider controls in various programs. There are probably more, but these are the ones that I thought were important:

  1. Give a visually attractive interface to the user to enable him to incrementally change the value of a variable in such a way that it is readily apparent what the approximate value is and will be when the slider is moved.
  2. Please go back and read item #1 and explain it to me when you understand it.
  3. The slider needs to be "selectable", meaning that when it is clicked on with the mouse, it is visually apparent that it is in fact "selected". The program user should also be able to tab to and from the control.
  4. While the slider is "selected", the user needs to be able to move the knob with the arrow, page, home, and end keys. The arrow keys move the slider in small increments, and the page, home, and end keys move the knob in larger increments.
  5. When the slider is clicked with the mouse in the areas surrounding the slider knob, the knob needs to move.
  6. It needs graduation marks to assist the user in the placement of the slider knob.
  7. It needs tooltips, and the MSG attribute.
  8. It needs to be written in Clarion as much as possible.

Step 2 – What will we build this thing out of?

Hmm… This is the hard part. There are so many different possible approaches here. Let’s see what we need, and if there is anything that we already have in CW that would serve the purpose:

  1. We need a "track" for the "knob" so slide upon. Luckily, with CW, there are several controls that would fit the bill. The track needs to be a 3D looking straight line, which can be accomplished with a control having the BEVEL attribute. That would include REGIONS, PANELS, and GROUPS. Since the track itself does not need to send or receive any events, a PANEL control is probably the best for this job.
  2. We need a "knob" to slide upon the track. Like the track, it needs to have the ability to have the BEVEL attribute. Unlike the track, it will need to be able to fire events, and be draggable. It looks like a REGION control will be the best for this job.
  3. Okay, now we have two tomato cans… I mean controls that will be used to make up our slider. Looks like we have everything that we need, except… Neither of these controls can receive focus. One of the things that we want to be able to do with this slider is to tab to and from it, and use the arrow/page keys to actuate it. We need a way to do this.
  4. It looks like we need another control that can receive focus. There are all types of Clarion controls that are capable of receiving focus. I decided to use an ENTRY control to handle the selectable part.
  5. We will also need one more control to receive mouse clicks so that the knob can be moved accordingly. Since a REGION can handle mouse clicks and be transparent, we will use one.

Step 3 – Programming

For the sake of semi-brevity, I will be generalizing quite a bit here.

OOP Classes

Each slider and its related controls are managed by the members (data variables) and methods (procedures and functions) in the slider class. Depending on whether the slider is configured for HORIZONTAL or VERTICAL use, different classes are used. Since a great deal of the necessary code is identical for horizontal as it is for vertical, they both inherit this identical code from the SliderClass class. Where the code needs to be different for horizontal than it does for vertical, the horizontal slider uses the HSliderClass class, and the vertical slider uses the VSliderClass class. This sounds a lot more complicated than it really is.

In the file "SLIDER.INC", the declarations for the classes are laid out. The actual code resides in "SLIDER.CLW". The SliderClass class contains all of the data members and methods that are used. Below the SLIDER class, there are the HSLIDER and VSliderClass classes. Notice in HSliderClass and VSliderClass, that they are using the SliderClass class in their declarations.

HSliderClass       CLASS( SliderClass ), TYPE, MODULE('slider.clw')
PreInit              PROCEDURE, VIRTUAL
Sync                 PROCEDURE( ? InitValue ), VIRTUAL
MousePos             FUNCTION(), LONG, VIRTUAL
SetToPosition        PROCEDURE( LONG NewPos ), VIRTUAL
SliderVal            FUNCTION(), REAL, VIRTUAL
CreateCTLS           PROCEDURE(*JSSIG IG), VIRTUAL
Graduate             PROCEDURE, VIRTUAL
                   END

The HsliderClass and VsliderClass both have identical method names, which are also the same as the methods declared in SliderClass. The reason that the methods are declared again, is so that they can OVERRIDE the current methods that exist in the SliderClass. By overriding the generic methods in the SliderClass, the Horizontal and Vertical can use custom tailored code that can be called by the existing non-overridden members of the SliderClass.. The VIRTUAL attribute allows the methods of the SliderClass to call the overriding methods of the vertical and horizontal classes, as if those methods had belonged to it in the first place.

About the MEMBERS

The data members are prefixed with an "m_". This is a tip that Ross Santos gave, which helps you to easily differentiate between data members and methods when reading the code. The members are the variables that store the data for each class. They can sort of be considered "Module Data" for each class. This allows each slider control to have its own personal set of variables to store information in.

m_SliderMax           REAL  !Upper limit of Slider value
m_SliderMin           REAL  !Lower Limit of slider value
m_SliderMinPos             SHORT !X or Y Position
m_SliderMaxPos        SHORT !X or Y Position
m_SliderCtl           SHORT !Field Number of the track
m_KnobCtl             SHORT !field number of the knob
m_KnobColor           LONG  !Color of the knob
m_KnobCenter          SHORT !Offset for the center of the knob
m_IsSliding           BYTE  !Currently sliding
m_MouseOffset         SHORT !X or Y Position
m_SizeProp            SHORT !Height or Width
m_PosProp             SHORT !X or Y Position
m_SelectCTL           SHORT !Entry control to Select()
m_ClickCTL            SHORT !Handles mouse clicks
m_SafeCreate          SHORT !Safe number to create new controls
m_Graduations         BYTE  !Number of graduations (tick marks)
m_Pixels              BYTE  !saves the status of PROP:Pixels
m_LargeUnits          LONG  !Large Scroll
m_SmallUnits          LONG  !Small Scroll

The Methods

The methods are the procedures and functions of the classes. They are all in the "SLIDER.CLW" file. I think that it would be too drawn out to give a "blow by blow" explanation for every line of code. Instead, here is a basic rundown of what they do:


Init              FUNCTION( ? InitValue, ? SliderMax ,? SliderMin , *JSSIG IG ),SHORT

The INIT() method is the first function that is called. It allows you (the programmer) to tell the class what window controls are being used, and specifics about the behavior and appearance of the slider.


EventHandler      FUNCTION( *? SliderUse ), BYTE

EventHandler() directs traffic as events are fired, and takes appropriate action. If there was a mouse click in the body of the slider, the ClickEvent() method is called. If the user is using the arrow keys to actuate the slider, then the KeyEvent() method is called. Any events pertinent to the slider performance are managed by EventHandler()


Sync              PROCEDURE( ? InitValue ), VIRTUAL

The Sync() Method is called when the slider needs to be set to a specific value. It positions the knob to where the USE variable’s value dictates.


MousePos          FUNCTION(), LONG, VIRTUAL

Depending on whether the Horizontal or Vertical class is being used, This function returns MouseX() or MouseY().


SliderVal         FUNCTION(), REAL, VIRTUAL

SliderVal() returns the calculated value based on the position of the slider.


SetToPosition     PROCEDURE( LONG NewPos ), VIRTUAL

Re-positions the slider knob to it’s new position, depending on Horizontal or Vertical.


CreateCTLS        PROCEDURE(*JSSIG IG), VIRTUAL

This procedure creates the transparent REGION and ENTRY controls used by the slider, and sets their properties.


Select            PROCEDURE

Selects the "selectable" control of the slider


Field              FUNCTION(), LONG

Used in the accept loop to determine if the current event() was generated by one of the controls within the class.


KeyEvent          PROCEDURE

Called by the EventHandler() function. If the event is generated by one of the alerted keys, then this procedure is called.


ClickEvent        PROCEDURE

Called by the EventHandler() function. If the event is generated by a mouse click to the center area of the slider, this procedure is called


Graduate          PROCEDURE, VIRTUAL

Draws the graduation lines for the slider


SelectBox         PROCEDURE

Uses API calls to draw a "Select" box around the slider.

Some Trials and Tribulations met along the way in the writing of this slider.

After kicking myself, I always wonder why something that is supposed to be so simple often ends up so complicated. Sometimes complication means that you are using the wrong approach. Sometimes simple things really aren’t as simple as they seemed 20 hours ago. There are cycles of great new ideas, elation, implementation, frustration, disappointment, and placing of blame on the innocent. However you get there, there is an end result – good or bad. In the following questions and answers, I will explain the logic (or lack thereof) behind some of the things in the code that look "unconventional".

Q: What is all of this PROP:Pixels stuff throughout the code?

A: You had to ask that, did you? In CW by default, a WINDOW uses DLUs (Dialog Units) as the unit of measure for setting and getting the size/position of controls. Depending on the window font, these DLU’s can be different sizes. When drawing the graduation lines, I found it to be quite a chore to evenly spread the desired number of lines from one end of the slider to the other. By temporarily setting the WINDOW to use PIXELS as the unit of measure, the lines easily spread out because I could more accurately place the lines where they should go.

Q: Okay, so why didn’t you force the window to use PROP:Pixels the entire time instead of flipping back and forth like you were doing?

A: I didn’t want to give any programmers an unpleasant surprise of finding out that any run-time measurements would be rendered incorrect by someone like me taking liberties with their setup. If they were setting the PROP:Width of a button to be 30 dlu’s, it would, instead be set to 30 PIXELS wide.

Q: With my keen sight, I noticed that you are not using the pixel measurement when positioning the knob. If you can get more accurate placement with pixels, why didn’t you use the pixel attribute when getting/setting positions of the slider knob?

A: Can’t stop with the pixel questions huh? Actually, I did try to implement what you had said, but it required way too much toggling of the PROP:Pixels mode in rapid succession. The end result was not pretty. The programmer, on the other hand does have the option of setting PROP:Pixels on the window of the procedure. That way, the slider class doesn’t have to worry about restoring the property, and the sliders will get the benefit of smaller increments.

Q: Oh I see. Good point. What is this AdjustOK variable? It looks like a flag to optionally allow the class to re-size the slider track. What would be the point of that?

A: Very good! That is what it is, alright. When the programmer sets this variable to TRUE, then the INIT() method automatically adjusts the size of the slider track so that it can be EVENLY divided by the number of "tick marks". If it cannot be evenly divided, then the graduation marks cannot possibly be evenly distributed along the length of the slider track. This makes it look quite unattractive. Allowing the INIT() method to do this for you, saves the time of re-sizing the track at design-time.

Q: What is the point of making the size of the slider knob an even number?

A: This is to simplify the code. By making it an even number, the slider can be "sliced" into two equal halves. This makes it a lot easier to position the slider equally on the beginning and end of the track.

Q: What is the point of the "Self.m_PosProp" and "Self.m_SizeProp"?

A: Depending on whether you are in a HORIZONTAL or VERTICAL slider, the Self.m_PosProp represents PROP:Xpos or PROP:Ypos. The Self.m_SizeProp represents either PROP:Width or PROP:Height. By using the Self.m_PosProp instead of PROP:Xpos, the very same code can be used for either a horizontal or vertical slider. Otherwise, the code would have to be duplicated for each slider, and I might as well have written two entirely different classes for each.

Q: Well, if you were able to use "Self.m_PosProp", etc. on some of the code, why couldn’t you do that sort of thing throughout the class instead of overwriting some of the methods?

A: Unfortunately that was not possible. There are some very different things done with the calculations of the horizontal and vertical sliders. For example, on a horizontal slider, the lowest value is typically when the slider knob is at the LEFT of the track, and the highest value is when the knob is at the RIGHT of the track. No problemo. That would mean that the smallest X-position would coincide with the smallest value and visa versa. BUT – on a vertical slider, the highest value coincides with the LOWEST Y-Position and visa versa. This means that the necessary calculations are completely different, and each type of slider needs it’s own set of methods to handle them.

Q: What is "Self.m_MouseOffset" used for?

A: That is there to confuse you and throw you off track. Actually, it is there to save the difference between the mouse position and the knob position when the mouse clicks on the knob. This way, when the mouse is moved while in "Sliding Mode", the same relative position between the knob and the mouse can be maintained throughout the movement of the mouse.

Q: Why do you have a PreInit() function?

A: Although I have been told that it is not a desirable form to use with OOP, I thought it was the best way to go in this case. By setting the m_PosProp and m_SizeProp members in the PreInit() method, there was much less code written for the Init() method than there would have been if I would have had to write it explicitly. Maybe in the future, when my capacity to grasp all of these design issues increases, it will make more sense to do it a different way.

The Template

The slider template is relatively straightforward. There is a GLOBAL extension template that must be populated.

Procedure Template

For each procedure that will have a slider, the PROCEDURE extension template must be added.

Image3.gif

The first prompt, "Highest Creatable Control", is a number that will be used to start creating extra window controls with. Make sure that this number is high enough to avoid conflict with any other window controls that you may have. Leaving it at the default setting is usually sufficient.

The second prompt, "Set Window to use Pixels" will cause your window to always use PIXELS as the unit of measure. If you set or get control sizes and positions at run-time, remember to keep this in mind if you keep this box checked.

Control Template

There are 2 control templates. One for Horizontal sliders, and one for Vertical. They are essentially the same, except they populate different configurations of window controls, and, of course, declare instances of different classes.

Image2.gif

The prompts are relatively straightforward.

"Field to Use" is the variable that will be updated by the slider.

"Max Range" is the value of the slider when the knob is at the maximum spot.

"Min Range" is the value of the slider when the knob is at the minimum spot.

"Graduations" is the number of "tick marks" that you want.

"AutoAdjust Size" specifies whether you want to allow the class to re-size your slider track to enable even spacing of the tick marks.

"Large Scroll" is the approximate value that you want the slider to change when you press the pageup, pagedown, home, and end keys. If left blank, then the size of the knob is used.

"Small Scroll" is the value that you want the slider to change when the arrow keys are used.

"MSG" is the MSG attribute. The text will appear in the window status bar when the slider is selected.

"ToolTip" is the tooltip.

NOTE: The tooltip is NOT RECOMMENDED when in 16-bit mode. The "Selected" box gets quite corrupted when the tooltip displays. In 32-bit mode, it works fine. I think that this is because of the native windows tooltips being used in 32-bit.

Are you through yet?

Yes, as a matter of fact, I am (almost). This slider code was primarily written as a tool to give me ideas for another project that I am working on. It was originally a very small, simple slider with limited functionality. It didn’t have the graduation marks, couldn’t be "selected", and the keyboard couldn’t be used, but it was a start. It is definitely not perfect now, but it is quite usable. Perhaps others will have some ideas to improve the functionality and will write some addendums to this offering. It was a fun exercise, and I hope that you enjoy it.

Printer-friendly version