Sunday, November 14, 2010

De-mystifying Windoze UpDown-Controls

Have you ever asked yourself why things are that complicated? If so, you might want to read further...

Windoze UpDown-Controls

Well, microsoft better had called them DownDown-Controls, because they are a cheap imitation of OS/2's sophisticated Spinbutton Controls. When I saw microsoft's documentation the very first time, I just skipped the implementation of my advanced spinbuttons after reading two or three pages. Meanwhile, I started to recreate DatTools, because I urgently need a tool to manage my datafields. I surely can do it with a hex editor, but - it is quite time consuming to enter strings this way...

Being forced to dive deeper into the complicated matter of Windoze' controlled Ups and Downs, I started to insert some experimental code into my raw DatTools skeleton. After writing a tool to retrieve all received messages and put them into a dump file, I finally got a clue how these controls really work. As it turned out, there is absolutely nothing complicated about it - except the HLL hocus pocus which hides really simple stuff behind important sounding goobledygook to keep normal people away from using these controls. Actually, Windoze' UpDown-Controls are less complicated than OS/2's Spinbuttons!

Umm ... this introduction is getting too large, let us start to learn something new. There are several ways to create an UpDown-Control. You can do it the really complicated way and use WinCreateUpDownControl() to create the control, but it is a mess to retrieve the proper x, y, w and h values for this function. The least complicated way to retrieve course parameters is to query the rectangle of the associated edit control, add a few pixel to (x + w) as new x position, use the same y and h and take h plus a few pixel as w parameter. The easiest way (for really lazy sods like me...) is to add one (or several) line(s) like this
CONTROL "", 0x138B, "msctls_updown32", 0x50010044,  85,  18,  15,  12
to your whatever.dlg file. The hexadecimal 0x50010044 is a short version of
UDS_ALIGNRIGHT | UDS_HORZ | WS_CHILD | WS_VISIBLE | WS_TABSTOP
These styles are defined for UpDown-Controls:
UDS_WRAP                        0x0001
UDS_SETBUDDYINT                 0x0002
UDS_ALIGNRIGHT                  0x0004
UDS_ALIGNLEFT                   0x0008
UDS_AUTOBUDDY                   0x0010
UDS_ARROWKEYS                   0x0020
UDS_HORZ                        0x0040
UDS_NOTHOUSANDS                 0x0080
UDS_HOTTRACK                    0x0100
Having done that, we can leave HeLL and start with some real work. Everything begins with processing the WM_INITDIALOG message. In general, you might want to set the upper and lower limits via something like
xorl     %eax,   %eax
xorl     %r9d,   %r9d
movq     %rdi,   %rcx
movl     $0x138B,%edx
incl     %eax
movl     $0x046F,%r8d
decl     %r9d
movq     %rax,   0x20(%rsp)
call     _SnDIM
SnDIM() is a wrapper for SendDlgItemMessageA(), preserving and restoring the eight registers destroyed by Win's API. RDI is my fixed storage for the dialog's HWND - I used RCX previously to pass parameters to other functions not shown here. After setting the lower and upper limits, you might want to tell the UpDown-Control where to start:
incl     %r9d
addl     $0x02,%edx
movq     %r9d, 0x20(%rsp)
call     _SnDIM
As you can see, wrappers save reloading all destroyed registers after each API call. Keep care to pass a zero in R09 (WPARAM), as well! The hexadecimal in RDX is the resource ID of the UpDown, the hexadecimal in R08 the message sent to the UpDown-Control. These are all messages you can send to an UpDown:
UDM_SETRANGE                    0x0465
UDM_GETRANGE                    0x0466
UDM_SETPOS                      0x0467
UDM_GETPOS                      0x0468
UDM_SETBUDDY                    0x0469
UDM_GETBUDDY                    0x046A
UDM_SETACCEL                    0x046B
UDM_GETACCEL                    0x046C
UDM_SETBASE                     0x046D
UDM_GETBASE                     0x046E
UDM_SETRANGE32                  0x046F
UDM_GETRANGE32                  0x0470
UDM_SETPOS32                    0x0471
UDM_GETPOS32                    0x0472
UDM_SETUNICODEFORMAT            0x2005
UDM_GETUNICODEFORMAT            0x2006
This is almost all of the complicated stuff to do. Oh, yes, not to forget - the only thing we have to evaluate after initialising the dialog is the WM_NOTIFY message. Whenever RDX holds 0x4E, we should check if the low word of R08 (WPARAM) is the ID of one of our UpDown-Controls. If so, R09 (LPARAM) holds the address of a 32 byte wide stack location where the following parameters are parked:
00   DQ   hwndFrom  control HWND
08   DQ   idFrom            ID
10   DQ   code      0xFFFFFD2E (UDN_DELTAPOS)
18   SD   iPos      position current
1C   SD   iDelta             new
You either can use iPos directly or add iDelta to the current value of your own parameter. Converting the new value to a hexadecimal or decimal string, selecting an entry from a string table or whatever else you want to do with the returned result is trivial and might be discussed in another post. There are a few messages UpDown-Controls may send in the code quadword:
UDN_FIRST                   0xFFFFFD2F
UDN_LAST                    0xFFFFFD27
UDN_DELTAPOS                0xFFFFFD2E
Now that you learned all important facts about UpDowns, It is time for some remarks.


Tips And Tricks

You might associate your UpDowns with the previous window (buddy window) in the dialog's window hierarchy, which probably is an edit control. If you want to implement some kind of true control over your controls, I strongly recommend not to associate your UpDowns with a buddy window. Let them work as simple controls with the ability to send WM_NOTIFY messages repeatedly as long as one of the arrow keys is pressed.

The most interesting detail is the iDelta parameter. As a matter of fact, this is the only useful thing an UpDown-Control emits. It keeps us informed about which of the buttons is held down currently. It is FFFFFFFF for DOWN and 00000001 for UP, when the UpDown starts spinning, and might be increased if one of the arrow keys is held down for a while. You can control this behaviour by setting the ACCEL structures associated with the UpDown to values suiting your needs. There are at least three ACCEL structure for each UpDown-Control. With these few parameters, it is easy to create very complex structures controlling multiple 'slave' windows with a single UpDown-Control.

The Future Is Less Than A Picosecond Away

It will take more than a few picoseconds to port my advanced spinbuttons to Windoze, but, nevertheless - let me introduce ST-Open's Spinbutton Library. Like all ST-Open Libraries, spinbuttons are controlled via datafields and provide an easy to use interface for assembler programmers as well as for C-style coders. All libraries follow standard C calling conventions. The entire programming interface consists of a single function, one common structure and a few line-filling definitions to keep C(+-*#?) programmers happy. What did we do if we were not forced to scroll the text on our 250 * 15,000 pixel screen sidewards? Quite unbelievable that any line of simple code was not sufficient to fill even 30,000 pixel wide screens, isn't it? But: Such code definitely exists, see above!

Back to business. The old function awaited four parameters
spin number
SPN_* command
numeric input    (optional)
address in/out   (optional)
where command was defined as one of these:
SPN_SET             0x08
SPN_GETCUR          0x07
SPN_GETID           0x06
SPN_GETSTRUC        0x05
SPN_QUERY           0x04
SPN_END             0x03
SPN_DN              0x02
SPN_UP              0x01
SPN_INIT            0x00
As mentioned above, the UpDown delivers all necessary parameters without lengthy requests to API functions. You can pass R08 and R09 through as you got them for SPN_NOTIFY and SPN_EDITED, while you have to provide one parameter for the other commands which are reduced to
SPN_SETSTRUC        0x06
SPN_GETSTRUC        0x05
SPN_QUERY           0x04
SPN_SET             0x03
SPN_EDITED          0x02
SPN_NOTIFY          0x01
SPN_INIT            0x00
where SPN_GETSTRUC and SPN_SETSTRUC only are required for HLL freaks (real programmers know how to read memory blocks without contorted manoeuvres). The positive side effects of datafield driven spinbuttons:
  • Due to the advanced conceptual design, code is greatly reduced and most tasks are perfomed by the highly optimised library function.
  • The spinbutton datafield is loaded automatically with the first SPN_INIT command if it is not present, yet.
  • All spinbuttons automatically start with the values of the last session without any line of extra code.
  • You have to set minimum and maximum values for each spinbutton only once.
  • Changing parameters, including the spinbutton type, is comfortably done with DatTools' spinbutton editor.
That much about the bread and butter side of ST-Open's Spinbuttons. Now the honey pot - the currently available spinbutton types:
SPN_STR             0x08
SPN_DATE            0x07
SPN_TIME            0x06
SPN_HEX64           0x05
SPN_HEX32           0x04
SPN_HEX16           0x03
SPN_HEX08           0x02
SPN_DEC64           0x01
SPN_DEC32           0x00
Are there any wishes left? Oh, well, there are no floating point spinbuttons, yet. As long as there are no FP conversions in my libraries, there are no FP spinbuttons. I never will code one more wrapper to call functions of a C library. Keep dirty programming where it belongs to (e.g. microsoft...). Fullstop.

Final Words

ST-Open's Libraries Version 8.0.0. (64 bit Win) will be available in a few months. If all libraries are tested, they will be uploaded to Google Code and I finally can start the development of IDEOS, the 21st century's operating system.

Addendum

Corrected some errors caused by mixed use of decimals and hexadacimals in some header files...

No comments:

Post a Comment