Posts Tagged set

Translating flags in API calls, part 2

As promised, this is part 2 of my article on translating flags in API calls. If you haven’t read part 1 yet, please do so now.

Sometimes, you get API calls that make real hefty use of flags. One such example is DrawFrameControl, which has one flag to specify the type of control to draw as well as a second flag to specify the style of that control. The values are all declared as DFC_ or DFCS_ constants and they may be freely combined. Only, not all combinations work together and a lot of developers combine the flags using the plus-operator instead of or, which certainly doesn’t help.

Following on from the last post, we can create several overloads, each of which contain only the list of values relevant to the type of control:

function DrawFrameControl(DC: HDC; const Rect: TRect;
  uType: TDrawFrameControlType; uState: TDrawFrameControlCaptionStyles): BOOL; stdcall; overload; external user32 name 'DrawFrameControl';
function DrawFrameControl(DC: HDC; const Rect: TRect;
  uType: TDrawFrameControlType; uState: TDrawFrameControlMenuStyles): BOOL; stdcall; overload; external user32 name 'DrawFrameControl';
function DrawFrameControl(DC: HDC; const Rect: TRect; uType: TDrawFrameControlType; uState: TDrawFrameControlScrollStyles): BOOL; stdcall; overload; external user32 name 'DrawFrameControl';
function DrawFrameControl(DC: HDC; const Rect: TRect; uType: TDrawFrameControlType; uState: TDrawFrameControlButtonStyles): BOOL; stdcall; overload; external user32 name 'DrawFrameControl';

Once you then specify your control in uType, the values you pass to uStyle will select the correct overloaded version of this function.

Of course, if it was that simple I wouldn’t have done a separate post about this. In the case of this function (and several others), the Windows API conspires against us.

First, we have the shared states. All your control types can have the following states:

DFCS_INACTIVE = $100;
DFCS_PUSHED = $200;
DFCS_CHECKED = $400;
DFCS_TRANSPARENT = $800;
DFCS_HOT = $1000;
DFCS_ADJUSTRECT = $2000;
DFCS_FLAT = $4000;
DFCS_MONO = $8000;

There is no really sensible way of combining these with your other control-specific states, other than to add these to each of your types. So the State type for button controls will look like this:

TDfcButtonStyle = (DFCSBUTTONRADIOIMAGE, DFCSBUTTONRADIOMASK,
  DFCSBUTTONRADIO, DFCSBUTTON3STATE, DFCSBUTTONPUSH,
  DFCSINACTIVE = 8, DFCSPUSHED,
  DFCSCHECKED, DFCSTRANSPARENT, DFCSHOT,
  DFCSADJUSTRECT, DFCSFLAT, DFCSMONO,
  DFCSBUTTONRANGE = 31);
TDfcButtonStyles = set of TDfcButtonStyle;

And you’d have something similar for all four types (buttons, captions, menus/popup menus and scroll bars). Once again, notice that DFCSINACTIVE gets a value of 8 instead of $100, because we are interested in the bit position.

The issue then of course becomes on of naming the values. You see, if you have DFCSFLAT in both TDfcButtonStyle and TDfcCaptionStyle the compiler will see them both and complain about the identifier being re-declared.

My favourite solution to this, is using the {$SCOPEDENUMS} compiler directive. This basically makes the Delphi compiler treat enumerated types in a way that is very similar to that of C# – the actual enumerated values are only in scope when you specify the type. I don’t like needing to think up unique two-character prefixes for each type anyhow, but in this case I find scoped enumerations to be invaluable.

Not only is TDfcButtonStyle.DFCSHOT now a distinct value from TDfcCaptionStyle.DFCSHOT, but I can in fact clean the names up to look more like it was thought up with mortals in mind:

TDfcButtonStyle = (ButtonRadioImage, ButtonRadioMask,
  ButtonRadio, Button3State, ButtonPush,
  Inactive = 8, Pushed, Checked, Transparent, Hot,
  AdjustRect, Flat, Mono, Range = 31);
TDfcButtonStyles = set of TDfcButtonStyle;

No doubt this technique will have lots of traditionalists up in arms, but I think it looks much cleaner and more readable.

The second issue is a little worse and in a twist of irony, is even worse when scoped enumerations are used.

You’ll notice there is no DFCBUTTONCHECKED or ButtonChecked in the examples above. That’s because this Windows constant has a value of zero. And Delphi set types don’t handle zero at all. You could pass [] for that parameter, but then it is no longer obvious that you are trying to draw a checkbox. Also, the compiler will have a torrid time distinguishing between the DrawFrameControl overloads.

I found two ways of getting this to work, but they both have the word hack scribbled all over them.

Firstly, you could add ButtonCheck to the above enumeration at some unused position, say 6. This works and is transparent to the client code, but the success of the technique depends entirely on the API function you’re calling. If it has an undocumented feature, or it changes in the next Windows version or it keels over when an unknown flag is passed in, you’re toast. Experience shows it to be mostly reliable, but it is still not to be fully trusted.

The second solution – the one that doesn’t like scoped enumerations – is to declare the missing value as a typed constant:

DFCSBUTTONCHECK: TDrawFrameControlButtonStyles = [];

This is future proof but has another issue – inconsistent use. Compare the calls to draw a checked radio button and a checked check box:

DrawFrameControlTS(PaintBox.Canvas.Handle, PaintRect,
  DFCBUTTON, DFCSBUTTONCHECK + [DFCSCHECKED]);
DrawFrameControlTS(PaintBox.Canvas.Handle, PaintRect,
  DFCBUTTON, [DFCSBUTTONRADIO, DFCSCHECKED]);

And no, I don’t have a magic trick that will make this work better.

So the take-home for this two-part article is this:

It is possible to create type-safe imports for Windows API functions that are every bit as natural to use as native Delphi functions. Some functions make strange use of flags and these can either use some of the described techniques to work around the hardship, or could be imported using good ol’ Cardinal.

As always, the situation and your own better judgement should serve as a guide.

Advertisement

Comments (2)

Translating flags in API calls, part 1

OK, so we have all seen the way that flags are passed to API calls. Take the ShowWindow function. In Delphi it is declared as this:

function ShowWindow(hWnd: HWND; nCmdShow: Integer): BOOL; stdcall;

Now, there is nothing wrong with the declaration – it will work, is well documented and is of course not seen by most Delphi developers, since the calls are all wrapped up in the Delphi VCL.

Thing is, that the nCmdShow parameter is declared as in integer (even in the Windows API docs) but functionally, it acts as an enumerated type. That is to say, it has a defined set of values, and only those values are permissible. What’s more, those values are sequential from 0 to 10.

Sounds familiar? Sound a little like an enumerated type, now doesn’t it? You can look up the imports used in Delphi in Windows.pas, but here is an alternative version:

function ShowWindow(hWnd: HWND; nCmdShow: TShowWindowValues): BOOL; stdcall;

and TShowWindowValues declared as:

type
  {$Z4}
  TShowWindowValues = (SW_HIDE = 0,
    SW_SHOWNORMAL = 1, SW_NORMAL = SW_SHOWNORMAL,
    SW_SHOWMINIMIZED = 2, SW_SHOWMAXIMIZED = 3,
    SW_MAXIMIZE = 3, SW_SHOWNOACTIVATE = 4,
    SW_SHOW = 5, SW_MINIMIZE = 6,
    SW_SHOWMINNOACTIVE = 7, SW_SHOWNA = 8,
    SW_RESTORE = 9, SW_SHOWDEFAULT = 10,
    SW_MAX = SW_SHOWDEFAULT);
  {$Z1}

OK, so a few things to note:

  1. The {$Z4} directive is necessary because the API type for the parameter is a four-byte integer and Delphi’s enumerated types are one byte in size by default.
  2. I specified the numeric values even though they are not specifically needed in this case (everything is sequential). This of course won’t work at all in Delphi 3 and earlier.
  3. Two of the values are assigned a value held by one of their siblings. So even when two values are equivalent, you can still use an enum.
  4. Given that the function is now type safe, it is even possible to change the names of the identifiers – fix the case and remove the underscores – if you feel like it, without confusing anyone.

How about flags that are combined?

These work differently and the values are not sequential. The idea being that the values can be mixed using the or operator. Consider the DrawEdge function:

function DrawEdge(hdc: HDC; var qrc: TRect; edge: UINT; grfFlags: UINT): BOOL; stdcall;

The edge parameter takes a combination of four values. These values are declared as:

BDR_RAISEDOUTER = 1;
BDR_SUNKENOUTER = 2;
BDR_RAISEDINNER = 4;
BDR_SUNKENINNER = 8;

Powers of two are used, so that when these values are added together (or combined with or) you would retain all the values. Because these are never meant to be used on their own, but rather in combinations, I like using sets:

TBorder = (BDR_RAISEDOUTER, BDR_SUNKENOUTER, BDR_RAISEDINNER, BDR_SUNKENINNER);
TBorders = set of TBorder;

At first glance, this may seem like a disaster. The values of the three enum values are 0, 1, 2 and 3 respectively, which is not as they are declared in Windows.pas.

But the way that set types work is identical to the way flags do. While the value of BDR_RAISEDINNER may be 2, the value of [BDR_RAISEDINNER] is in fact 4. Two to the power of two is four, remember?

Some readers may know already that the declarations above don’t work. The concept is fine, but the type sizes are all wrong. TBorders is one byte in size, not four. And {$Z4} does nothing to change it.

You see, set values are stored as bit flags, but the size is determined by the range of the ordinal type. I have four values in TBorder, so TBorders only uses the four least significant bits of a byte. So how do we get TBorders to be four bytes in size? Easy, we add a value that should be stored in bit 31:

TBorder = (BDR_RAISEDOUTER, BDR_SUNKENOUTER, BDR_RAISEDINNER, BDR_SUNKENINNER, BDR_Range = 31);
TBorders = set of TBorder;

The extra BDR_Range value stretches TBorders to four bytes and we have a type safe version of DrawEdge.

There is more to come, but I think that will wait for the next post.

First, there is something that I want to clear up. Suppose you read this and you like the concept, now what? ShowWindow and DrawEdge both have their definitions in Windows.pas and I am in no way suggesting that these must be changed or that we should all import them separately.

The one (secondary) point here is that the declarations in Windows.pas are almost certainly generated by some tool. If Nick and the boys were to become real inspired they could change the tool to generate type safe overloads. There are probably some good reasons why this was not done from the start (you couldn’t assign numeric values to enum elements before Delphi 4 is one that comes to mind), but it is still a good idea.

More generally though, there are many APIs that Delphi doesn’t have imports for. Lots of programmers import these and some even share large repositories of them. When you find the need to do an API import, there is nothing stopping you from making it type safe.

In part 2, I’ll discuss what to do when your API call has a conditional set of values for a parameter – check out DrawFrameControl if you want a preview.

Update: Part 2 has now been published.

Comments (1)