Creating a Palette or floating toolwindow


This example uses the source in procedure winstyle.p available in WinStyle.p.

A palette, or floating toolwindow should have these three features:
* a small title bar
* no associated button on the Taskbar because it is considered a popup-window of its 'client'
* stays on top, at least relative to the window it is 'serving'

The first two features are done automatically when pushing the WS_EX_PALETTEWINDOW style. Controlling the behavior of 'stay-on-top' requires some extra work in P4GL.

Controlling the Stay-on-top behavior

When a window has the Topmost style it will always be visible in front of all other windows that don't have the Topmost style. That is practically on top of all other windows. So when you create a palette in Progress with the Topmost style and switch to a different application (say, a web browser) you will still see the palette on top of the web browser. That is more than you bargained for and can not be tolerated.
What you really want is a window that is only Topmost relative to other windows in the same application (or in the same thread or in the same process, that's all pretty similar) and it is surprising that Windows does not support that requirement. So we will have to code it ourselves.
A floating toolbar is typically invoked by and working for one particular window. The same can be assumed for a palette window although the Progress UIB shows an exception to this 'rule'. For simplicity sake I will work with the assumption there are two windows involved: the Main window and its Palette window. The goal is: if win-Main is active we must assure that win-Palette has the Topmost style, if focus moves to a different window we must assure that the Topmost style gets removed from win-Palette.
So we will use the ON ENTRY and ON LEAVE triggers of win-Main.
This approach has one documented bug: if you leave win-Main to activate a different application you get no ON LEAVE event. Oh well, the user will click the win-Palette in an impulse and thus trigger the ON LEAVE of win-Main... it's not great but better than before.
To get a reliable ON LEAVE you should have to subclass the win-Main, but that's too far from 4GL for now. If required you might try the MsgBlaster control.

Examples

There are at least three procedure files involved: winmain.w implementing win-Main as mentioned above, palette.w implementing the palette window and winstyle.p being a library of procedures where the WS_EX_PALETTEWINDOW style is applied. Let's go:

/* some fragments from winmain.w */
DEFINE VARIABLE hPalette AS HANDLE un-undo.
 
ON ENTRY OF win-Main
DO:
   RUN SetTopMost IN hPalette(YES) NO-ERROR.
END.
 
ON LEAVE OF win-Main
DO:
   RUN SetTopMost IN hPalette(NO) NO-ERROR.
END.
 
ON CHOOSE OF BUTTON-palette IN FRAME DEFAULT-FRAME /* Show Palette */
DO:
  RUN SetTopMost IN hPalette(YES) NO-ERROR.
  IF ERROR-STATUS:ERROR THEN DO:
     RUN Palette.w PERSISTENT SET hPalette.
     RUN SetTopMost IN hPalette(YES) NO-ERROR.
  END.
END.
/* some fragments from palette.w */
{windows.i}
 
/* add to Mainblock: */
  DEFINE VARIABLE hStyle AS HANDLE NO-UNDO.
  RUN WinStyle.p PERSISTENT SET hStyle.
  RUN AddPaletteStyle IN hStyle ({&window-name}:HWND).
  DELETE PROCEDURE hStyle.
 
/* add internal procedure: */
PROCEDURE SetTopMost :
/*------------------------------------------------------------------------------
  Purpose:     prevents overlapping on other applications
  Parameters:  logical TopMost : yes = switch TopMost on
                                 no  = switch TopMost off
------------------------------------------------------------------------------*/
  DEFINE INPUT PARAMETER TopMost AS LOGICAL NO-UNDO.
 
  DEFINE VARIABLE hNonClient AS INTEGER NO-UNDO.
  DEFINE VARIABLE hwndInsertAfter AS INTEGER NO-UNDO.
  DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO.
 
  hNonClient = GetParent({&window-name}:HWND).
  IF TopMost THEN 
     hwndInsertAfter = {&HWND_TOPMOST}.
  ELSE
     hwndInsertAfter = {&HWND_NOTOPMOST}.
 
  RUN SetWindowPos IN hpApi
    ( hNonClient,
      hwndInsertAfter,
      0,0,0,0,    /* x,y,width,height : will be ignored */
      {&SWP_NOMOVE} + {&SWP_NOSIZE} + {&SWP_NOACTIVATE},
      OUTPUT ReturnValue
    ).
END PROCEDURE.
 

Explanation

The above source is pretty straightforward, I think. The contents of procedure AddPaletteStyle is another cup of tea, let's take a look:
First, the procedure needs to determine the hwnd of the NonClient-area because that's the window that owns the title bar (or 'Caption' in ms-talk). It finds it by repeatedly calling GetParent until it finds a window with a caption.
It is very weird to set the WS_EX_PALETTEWINDOW style when the window is already realized. To make it easier for Windows we first hide the window by calling ShowWindow(hwnd,0,output RetVal), this results also in the hiding of the Taskbar button. If you would not hide the window it would get the WS_EX_PALETTEWINDOW style alright but the Taskbar would become a mess, since Windows didn't really anticipate this.
Since we used ShowWindow to hide the window you might expect that we also use ShowWindow in the end to show it again. No need, because we already needed SetWindowPos we might as well give it the extra SWP_SHOWWINDOW option.
Because the caption shrinks, the entire window must shrink or else there will be a gap between the caption and the client-area. Procedure 'FitFrame' implements api-calls for calculating the required size for the NonClient-area.