This collection of code snippets used to be hosted on www.global-shared.com.
It is an old collection: it all began in 1996 or 1997, when Progress 8 was new and many of us were still using Windows 3.1 !!
Unlike wine or paintings, program code doesn't get better when it ages. You can find fragments that can be improved because Microsoft continuously expands their API, or are outdated because Progress has added features to the ABL so we don't need to use the WIN32 API anymore.
In a few days it will be 5 February, the 10th aniversary of the “Progress Reference to Windows API”. It will also be the very last day of the global-shared.com domain.
It all started when Progress 7 was brand new; the first Progress version with widgets and persistent procedures. It was also the first Progress version designed for the Windows operating system and this was quite a culture shock for lots of Unix-oriented Progress developers who were used to editing loops and readkey statements.
Because of this, PEG got swamped for a while with questions how to deal with Windows, and there was one particular queston that was asked over and over again. I am not sure but I think it was something like “how do you launch an external program”. I happened to know the answer and repeated it every time the question came along, until I decided to write this single Q&A on my personal homepage, so I could simply point to it whenever the question was raised again. But if you have one Q&A you might as well add a second or a third, and a website was born…
After a while I had to move away from the personal homepage and was given the opportunity to put the API pages in a subdirectory of www.pugcentral.org and after yet another while, in februari 2000, I decided to register a new domain: www.global-shared.com
Mind you, it was a different time back then! While Microsoft and Borland users had real communities with user-contributed code samples and downloads, there was no such thing for Progress. There was almost no 4GL code on the internet except hidden in PEG discussions, there were no Open Source projects in 4GL and many developers seemed keen to keep their secrets to their own. Or maybe I just could not find any, because “progress” was not such a great search term for Altavista J
Anyway, my goal with www.global-shared.com was to start sharing, in both directions, because I was convinced that you will eventually receive code examples when you start by giving some away.
I think I can say that it worked, at least for a while. I have contributed a bunch of API examples myself and then other people also volunteered and contributed theirs. The end result is a fair collection of examples, of which only a small part comes from my desk.
Of course, the collection has stopped growing. Perhaps because Windows is not new or scary to Progress programmers anymore, or perhaps because it got more attractive to search MSDN and port their code to 4GL, who knows.
There were times when global-shared.com was not just about API examples. I was still concerned that Progress-related websites were so difficult to find, and that there were so little free downloads or Open Source projects. So global-shared.com also had an open, user-maintained collection of links, I have initiated the “Progress Developer’s Webring” and started the Prolint open source project with a lot of help from John and Judy.
Although the Webring seemed like a good idea at the time, I really did not like to manage it anymore and abandoned it. The ring is adopted by Greg Higgins.
Prolint is still a good idea, but it outgrew global-shared.com and moved away to its own domain (prolint.org) and is now here at the OpenEdge Hive (http://oehive.org/prolint)
So in the end, without Prolint or other exciting stuff, global-shared.com became just a pretty static collection of API examples, some of which were getting old too. You could say it was just an archive… up and running but not running hard… dormant, awaiting new opportunities.
The OpenEdge Hive is this new opportunity. The Hive is everything what global-shared always wanted to be, and more!
Global-shared.com has done its job and will now retire. February 5, 2007 is the last day for the old domain. All content has already moved to The Hive (see oehive.org/win32api), it gets a new audience, it has new tools for collaboration.
I thank you all for the past 10 years.
.
The original question was: where do I learn about the differences between DLL and COM/OLE and so on, so I don't confuse one with the other and know the limitations/features of each.
Jeez I dunno... where to start? Please help me out by editing this topic whenever you want to change something!
A DLL (Dynamic Link Library) is much like a persistent procedure in Progress. In Progress, the persistent procedure contains one or more internal procedures and one or more functions, private or not. The IP's and UDF's that are not private can be called "from outside", provided that you know the name and the parameters and that you have any clue what kind of action the IP/UDF will perform.
Likewise, a DLL contains one or more functions, published or not. (it is uncommon to have procedures instead of functions). You can load the DLL persistent with LoadLibrary, call the published functions and finally use FreeLibrary to get rid of the persistent DLL. As with a Progress persistent procedure, you need to have some sort of written documentation to find the function names, their parameters and expected behaviour.
DLL's work well although problems may rise - especially in setup, maintenance and documentation.
Setup: where should you install these things - before an application can load a DLL it must first try to locate it. You could put it in a subdirectory of your application, but that is not practical if you have many applications (like Microsoft has) and want to reuse common components. You could put them in a shared directory, like windows/system32, but then you enter the maintenance nightmare.
Maintaining a DLL can become a problem both for the programmer and for the end-user. How can you add a new function to an existing DLL, or even a new parameter to an existing fuction, and still make sure that you don't get in trouble with the user base? Since a single DLL can be used by several different applications, there is no way of forcing all those application vendors to simultaniously create an update that matches with the new DLL version.
Documentation: a DLL is a black box, it does not contain its own documentation. It becomes messy when you have a lot of programmers writing DLL's, especially if the company is so big that there are different cultures between departments and different parameter styles you can't get used to.
So even though DLL work well technically, there was a need to invent a way to have DLL's document themselves, publish their file locations to the apps that needed to use them, and to be somewhat version independent.
An ActiveX control is basically just a DLL with some extra functions in it. One of those functions is RegisterServer, which writes the location of the DLL in the registry database under some unique identifier. A calling application does not need to know beforehand where the DLL is located, it only needs to know the unique identifier and can then locate the DLL by looking in the registry, before it loads that DLL into memory. (I say DLL but most of these have the extension OCX, although they are really just DLL with extras). So you don't call LoadLibrary directly, you call it through a wrapper function (not sure but I believe it is CoCreateInstance).
An application is not supposed to call any of the built-in functions directly, because of the risk that you call the wrong version of a function. Instead, the app needs to query the supported interfaces first. To make a long story short, the OCX sends the application a table of built-in functions that can be used. The OCX might support several versions but not all versions can be mixed. Suppose for example an OCX that contains functions AddCustomer and DeleteCustomer, and both have a version 1 and a version 2. One application might want to call AddCustomer version 1 and DeleteCustomer version 1, an other app might want to use version 2 of the interface. A mix is not allowed, e.g. AddCustomer version 2 and DeleteCustomer version 1 will have undesired results. So the OCX sends a table of alllowed functions to the app, with functionpointers to the functions that match the version of the calling application. (in reality there is probably only one function that has different versions. In that case, version 2 of the interface just points to the one and only version of the other functions)
An OCX is typically linked to a TLB resource (Type Library) that contains documentation for the interfaces. Not just for human eyes, but also for the calling application that can now automatically validate the number and type of parameters. This also means that parameter types needed to be standarized, which was not the case with bare DLL's.
Responding to events:
For a normal DLL it is pretty difficult to raise an event that can be handled in the calling application. For this to work, the calling application should have defined an exported function and must have told the DLL what the funcionpointer is for this callback function. (Progress 4GL does not support the definition of exported functions, so this does not work for us). ActiveX has several solutions for this problem, the **eventsink** is most commonly used. The application queries not only the interface for functions that can be called in the OCX, but also queries for a different interface to receive a list of expected callback-functions. The TLB plays an important role here. The received table contains a list of functions that the OCX expects to find in the calling application. The calling app can now dynamically create an eventsink (in its runtime module): a block of dynamic functions that do nothing but forward the call (from the OCX) to the event-handler procedure that you have written (in 4GL). Well actually they do a little bit more than that: they also have to take care of parameter translation.
About parameter tranlation and mashalling:
Back to the TLB for a moment. The TLB describes parameters in standarized datatypes. The host application (Progress) may have to transform the bits and bytes of the actual parameter (like, a character string) to whatever structure the OCX expects (like, a BSTRING). For simple types like integer this may be trivial, but for more complex types like datetime, fonts, colorref, arrays or even a logical it may involve some more work. In Progress this is all handled by the runtime module, although not all types are fully supported yet (like the time part in a datettime structure, or a variant array).
Marshalling guards you against hostile pointers. Remember the oldfashioned DLL API: you define a memory pointer in your application and store a bunch of data behind it. You then pass the pointervalue to the DLL, which can now read and even write the data directly. Works like a charm, until mistakes happen - the data segment may become corrupted and the application may crash, or worse. This is undesirable for a client, but even worse for a server process or a background process. OCX protects you by not allowing to pass pointers - this includes also character strings and output parameters. Instead of passing the pointer itself, the data behind the pointer is packed in a protected envelope and transferred to the OCX and back. Of course this is only possible if the structure of whatever the pointer is pointing to, is well-known and standarized.
Well, for one thing, you cannot pass pointers to an OCX.
what else? I suppose we need to explain what's going on with instance counters and RELEASE OBJECT...
Ok, the story is unfinished, I give up for now. I might return later to write some more... in the meantime you're welcome to take over
by Theo Albers
When you write your own OLE Automation component in C++ (ATL 3.0 of Microsoft Visual Studio 6+) you will experience the problem that Progress doesn't handle the OLE-events. The "OLE COM"-viewer of Progress is able to show proper events and methods, but the 4GL code simply won't be triggered.
In cooperation with Progress I was able to figure out the problem: when Progress registers for event subscription, it needs to be called back using IDispatch. This is in contrast to other clients like Visual Basic or Windows Scripting Host, which implement the event interface. For more information see the Progress knowledge base entry P56004. For more information on IConnectionPointContainer.Advise() see for instance http:builder.com.com/5100-6373-1050003.html and http:www.techvanguards.com/com/concepts/connectionpoints.asp.
I have attached an ATL sample which shows the usage of a simple OLE component in VB, JavaScript and 4GL. When you want to write your own OLE Automation object, take a look at the Advise() code of MsgQueue.cpp. This is the only part that needs modification when your client is a Progress client.
Override this method to add another check for IDispatch when Progress is calling this method!
STDMETHODIMP CMsgQueue::Advise(IUnknown *pUnk, DWORD *pdwCookie)
{
#ifdef DEBUG_PRINT
AfxMessageBox("In Advise()");
#endif
OK. This function is the important one. This is the place
where we store event sink object for future reference.
HRESULT hr = E_UNEXPECTED;
First we need to make sure that our pointers are valid.
if (0 == pUnk) return E_POINTER;
if (0 == pdwCookie) return E_POINTER;
_IMsgQueueEvents *pEvt = 0;
INTEGER type = 0;
hr = pUnk->QueryInterface(__uuidof(_IMsgQueueEvents), (void **)&pEvt);
if (SUCCEEDED(hr))
{
#ifdef DEBUG_PRINT
AfxMessageBox("Advise()--> sink is IMsgQueueEvents");
#endif
type = 1;
}
else Workaround for Progress, which doesn't implement the event source, but simply provides IDispatch
{
#ifdef DEBUG_PRINT
AfxMessageBox("No");
#endif
hr = pUnk->QueryInterface(__uuidof(IDispatch), (void **)&pEvt);
if (SUCCEEDED(hr))
{
#ifdef DEBUG_PRINT
AfxMessageBox("Advise()--> sink is IDispatch (how do we trust this?)");
#endif
type = 2;
}
else
{
#ifdef DEBUG_PRINT
AfxMessageBox("Advise()--> sink is invalid");
#endif
return CONNECT_E_CANNOTCONNECT;
}
}
Lock();
*pdwCookie = m_vec.Add(pUnk);
hr = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT;
Unlock();
if (hr != S_OK)
pUnk->Release();
if (FAILED(hr))
*pdwCookie = 0;
return hr;
}
atlqueue.zip : C++ example of working OLE Automation events
Here is an alarming e-mail from Brent Wardle to the Peg. (I know one should no copy e-mails from one forum to antoher, but this one really needs attention in the context of this website).
Hi Peg,
We are upgrading from 9.1D to 10.B and I found an issue tonight that you
may or may not know about.
You can no longer define and use a character output parameter for a
windows DLL call.
You do not get an error message during compile but at run time you get:
"You cannot use OUTPUT to return CHAR or LONGCHAR data. Use MEMPTR
Instead (12200).
Error 12200 is not available on KB error code search.
This impacts some of the DLL calls used in things like GetHostName.p
which is/was used in smtpmail.p and other deadline saving
global-shared.com / FFW code.
Calls like:
PROCEDURE gethostname EXTERNAL "wsock32.dll" : DEFINE OUTPUT PARAMETER p-Hostname AS CHARACTER. DEFINE INPUT PARAMETER p-Length AS LONG. DEFINE RETURN PARAMETER p-Return AS LONG. END PROCEDURE.
Cause the error.
p-Hostname must now be a MEMPTR and must be sized before and cleaned up
after the call.
Not sure if this impacts Unix/Linux shared lib calls.
I have received many code examples (thank you) but several use parameters of type MEMPTR where a CHARACTER would be more effective, in my opinion.
Since this seems to be a common issue, I will try to explain what's going on.
Most procedure declarations are derived from text in Windows API reference books or helpfiles that are aimed at C programmers. A typical example would be (this is 32-bit but the theory also applies to 16-bit) :
The GetProfileString function retrieves the string associated with the specified key in the given section of the WIN.INI file. This function is provided for compatibility with 16-bit Windows-based applications. Win32-based applications should store initialization information in the registry. DWORD GetProfileString( LPCTSTR lpAppName, // points to section name LPCTSTR lpKeyName, // points to key name LPCTSTR lpDefault, // points to default string LPTSTR lpReturnedString, // points to destination buffer DWORD nSize, // size of destination buffer );
All these typedefs starting with 'lp' are 'long pointer' to something and the linecomments also clearly say "points to..."
So it is fully understandeble that you would translate this to Progress like this:
PROCEDURE GetProfileStringA EXTERNAL "kernel32" : DEFINE INPUT PARAMETER lpAppName AS MEMPTR. DEFINE INPUT PARAMETER lpKeyName AS MEMPTR. DEFINE INPUT PARAMETER lpDefault AS MEMPTR. DEFINE INPUT PARAMETER lpReturnedString AS MEMPTR. DEFINE INPUT PARAMETER nSize AS LONG. END PROCEDURE.
When you use this function to read a value from an ini file, you probably have character variables (or literals) for the section, key name and default string.And now you have to convert them to and from MEMPTR variables first. So you would have to declare variables of type MEMPTR, allocate them (with set-size) and put the strings in them (with put-string). Then you can call the GetPrivateProfileString procedure. Finally you would have to use get-string to get the answer from lpReturnedString.
To me that looks like a lot of work for passing some strings.
The good news is that it does not have to be so difficult.
A LPCTSTR (or LPTSTR or LPSTR et cetera) is a long pointer that points to the memory location where the first character of a string is stored and subsequent memory locations are occupied by the subsequent characters of the string bla bla bla...
In other words: a LPCTSTR simply points to a string.
A Progress character variable is actually implemented as a memory pointer that points to a string.
As you see, a Progress character variable actually IS compatible with those LPCTSTR-like typedefs! Knowing this, the next procedure definition is valid:
PROCEDURE GetProfileStringA EXTERNAL "kernel32" : DEFINE INPUT PARAMETER lpAppName AS CHARACTER. DEFINE INPUT PARAMETER lpKeyName AS CHARACTER. DEFINE INPUT PARAMETER lpDefault AS CHARACTER. DEFINE OUTPUT PARAMETER lpReturnedString AS CHARACTER. DEFINE INPUT PARAMETER nSize AS LONG. END PROCEDURE.
As you see, you can now simply call this procedure with Progess character variables. No conversions are needed.
Actually, there is one thing to remember: Windows will NOT allocate memory for the lpReturnedString so you will have to do that yourself. The nSize parameter tells Windows how much memory you have allocated, so it will not write more data than nSize into the lpReturnedString. If you would provide a value for nSize larger than length(lpReturnedString), you might get to see a General Protection Failure because Windows might try to write past the end of your string.
Allocating memory is simply done with the FILL statement.
Here's an example of how to do it:
DEFINE VARIABLE Printername AS CHARACTER NO-UNDO. Printername = FILL(" ", 100). /* = allocate memory for 100 chars */ RUN GetProfileStringA("windows", "device", "-unknown-,", OUTPUT Printername, LENGTH(Printername)). printername = ENTRY(1,printername).
The "C" in LPCTSTR tells us this is a constant; the DLL will not modify the contents of this parameter. You can translate it to DEFINE INPUT PARAMETER ... AS CHAR.
LPSTR and LPTSTR parameters (without a "C") are no constants; their contents will be modified by the DLL so these will typically be translated to OUTPUT or INPUT-OUTPUT parameters.
API functions return null-terminated strings, that is: a couple of relevant characters terminated by CHR(0) and possibly followed by random characters. This may (or will) cause problems especially if you use the returned string to be concatenated with a second string and send the result to another C-function.
For example: suppose you want to create a temporary file and call function GetTempPathA to get the name of the temp-directory in variable chTempdir. You decide the tempfile should be named chTempfile = chTempdir + "\myfile.tmp" and use this as input parameter to some other C-function. The C-funtion will not process the "\myfile.tmp" part because it only reads up to the CHR(0) character.
So how to deal with this terminating null? Well, some functions tell you where the null is, others don't. For example, GetTempPathA returns the length of the relevant string so you can use this value to trim the result:
DEFINE VARIABLE chTempPath AS CHARACTER NO-UNDO. DEFINE VARIABLE ReturnedLength AS INTEGER NO-UNDO. chTempPath = FILL(" ", MAX_PATH). /* = 260 */ RUN GetTempPathA( LENGTH(chTempPath), OUTPUT chTempPath, OUTPUT ReturnedLength). IF ReturnedLength>0 AND ReturnedLength<=MAX_PATH THEN chTempPath = SUBSTRING(chTempPath,1,ReturnedLength). /* Some other functions do not tell you the length of the returned string. In that case you can safely use ENTRY(1,identifier,CHR(0)) like in this code snippet: RUN gethostname (OUTPUT w-TcpName, INPUT w-Length, OUTPUT w-Return). */ /* Check for errors */ IF w-Return NE 0 THEN DO: MESSAGE "Error getting tcp name." VIEW-AS ALERT-BOX. RUN WSACleanup (OUTPUT w-Return). RETURN. END. /* Pass back gathered info */ /* remember: the string is null-terminated so there is a CHR(0) inside w-TcpName. We have to trim it: */ p-TcpName = ENTRY(1,w-TcpName,CHR(0)).
Thanks to Joern Winther for the ENTRY(1,identifier,CHR(0)) hint.
.
A window is not automatically centered to the screen, but is cascaded.
This procedure takes the widget-handle of a window as input parameter and centers the window to the 'working area'.
The working area is the portion of the screen not overlapped by the taskbar. So the result will differ when you move the taskbar to either of the 4 edges of the screen.
Windows 98 and Windows 2000 support multiple monitors. The monitors share a virtual desktop but each monitor has its own working area. This procedure is improved to center the window to the monitor where it is positioned at that time. That is, if the window is positioned somewhere on the secondary monitor it will be centered to the secondary monitor too.
The window will not dynamically stay centered using this procedure: if the size of the window changes or if the size/position of the taskbar changes, you will have to run this procedure again.
{windows.i} PROCEDURE CenterWindow : /*------------------------------------------------------------------------------ Purpose: centers window to the working area. ("working area" is portion of screen not obscured by taskbar) Parameters: winhandle : progress widget-handle of a window widget Notes: ------------------------------------------------------------------------------*/ DEFINE INPUT PARAMETER winhandle AS HANDLE NO-UNDO. IF LOOKUP(winhandle:TYPE , "window,dialox-box":U ) = 0 THEN RETURN. /* calculate coordinates and dimensions of working area */ DEFINE VARIABLE workingleft AS INTEGER NO-UNDO. DEFINE VARIABLE workingtop AS INTEGER NO-UNDO. DEFINE VARIABLE workingwidth AS INTEGER NO-UNDO. DEFINE VARIABLE workingheight AS INTEGER NO-UNDO. DEFINE VARIABLE lpWorkingRect AS MEMPTR. /* RECT structure */ DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpWorkingRect)=4 * {&INTSIZE}. RUN GetWorkingArea (winhandle:HWND, lpWorkingRect). /* RECT is filled with left,top,right,bottom */ workingleft = get-{&INT}(lpWorkingRect,1 + 0 * {&INTSIZE}). workingtop = get-{&INT}(lpWorkingRect,1 + 1 * {&INTSIZE}). workingwidth = get-{&INT}(lpWorkingRect,1 + 2 * {&INTSIZE}) - workingleft. workingheight = get-{&INT}(lpWorkingRect,1 + 3 * {&INTSIZE}) - workingtop. /* calculate current coordinates and dimensions of window */ DEFINE VARIABLE windowleft AS INTEGER NO-UNDO. DEFINE VARIABLE windowtop AS INTEGER NO-UNDO. DEFINE VARIABLE windowwidth AS INTEGER NO-UNDO. DEFINE VARIABLE windowheight AS INTEGER NO-UNDO. DEFINE VARIABLE hParent AS INTEGER NO-UNDO. DEFINE VARIABLE lpWinRect AS MEMPTR. SET-SIZE(lpWinRect)=4 * {&INTSIZE}. hParent = GetParent(winhandle:HWND). RUN GetWindowRect IN hpApi(hParent, GET-POINTER-VALUE(lpWinRect), OUTPUT ReturnValue). windowleft = get-{&INT}(lpWinRect,1 + 0 * {&INTSIZE}). windowtop = get-{&INT}(lpWinRect,1 + 1 * {&INTSIZE}). windowwidth = get-{&INT}(lpWinRect,1 + 2 * {&INTSIZE}) - windowleft. windowheight = get-{&INT}(lpWinRect,1 + 3 * {&INTSIZE}) - windowtop. /* calculate new x and y for window */ windowleft = workingleft + INTEGER((workingwidth - windowwidth ) / 2 ). windowtop = workingtop + INTEGER((workingheight - windowheight ) / 2 ). /* perhaps you should ensure that the upper-left corner of the window stays visible, e.g. user can reach system-menu to close the window: */ windowleft = MAXIMUM(workingleft, windowleft). windowtop = MAXIMUM(workingtop, windowtop). /* assign these values. No need to use API: */ ASSIGN winhandle:X = windowleft winhandle:Y = windowtop. /* free memory */ SET-SIZE(lpWorkingRect) = 0. SET-SIZE(lpWinRect) = 0. END PROCEDURE. PROCEDURE GetWorkingArea : DEFINE INPUT PARAMETER HWND AS INTEGER NO-UNDO. DEFINE INPUT PARAMETER lpRect AS MEMPTR NO-UNDO. DEFINE VARIABLE hMonitor AS INTEGER NO-UNDO. DEFINE VARIABLE lpMonitorInfo AS MEMPTR. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE SimpleArea AS LOGICAL NO-UNDO INITIAL NO. IF NOT (RunningWindows98() OR RunningWindows2000()) THEN SimpleArea = YES. ELSE DO: RUN MonitorFromWindow(HWND, 2, OUTPUT hMonitor). IF hMonitor = 0 THEN SimpleArea = YES. ELSE DO: SET-SIZE(lpMonitorInfo) = 4 + 16 + 16 + 4. PUT-LONG(lpMonitorInfo, 1) = GET-SIZE(lpMonitorInfo). RUN GetMonitorInfoA(hMonitor, GET-POINTER-VALUE(lpMonitorInfo), OUTPUT ReturnValue). IF ReturnValue = 0 THEN SimpleArea = YES. ELSE DO: PUT-LONG(lpRect, 1) = GET-LONG(lpMonitorInfo, 21). PUT-LONG(lpRect, 5) = GET-LONG(lpMonitorInfo, 25). PUT-LONG(lpRect, 9) = GET-LONG(lpMonitorInfo, 29). PUT-LONG(lpRect,13) = GET-LONG(lpMonitorInfo, 33). END. SET-SIZE(lpMonitorInfo) = 0. END. END. IF SimpleArea THEN RUN SystemParametersInfo{&A} IN hpApi ( 48, /* 48 = SPI_GETWORKAREA */ 0, GET-POINTER-VALUE(lpRect), 0, OUTPUT ReturnValue). END PROCEDURE. PROCEDURE MonitorFromWindow EXTERNAL "user32" : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER dwFlags AS LONG. DEFINE RETURN PARAMETER hMonitor AS LONG. END PROCEDURE. PROCEDURE GetMonitorInfoA EXTERNAL "user32" : DEFINE INPUT PARAMETER hMonitor AS LONG. DEFINE INPUT PARAMETER lpMonitorInfo AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.
Can probably do with less variables but I wanted a certain degree of readability.
The procedure has to rely that the input parameter is indeed of type "window". In other procedures (in winstyle.p) I used to traverse up the GetParent chain until a window with a caption was found. That would not work for a Splash screen.
Can't use 4GL attributes to find the size of a window, because winhandle:width-pixels and winhandle:height-pixels return the dimensions of the Client area. It is convenient but confusing that winhandle:x and winhandle:y are the coordinates of the NonClient area.
procedure SystemParametersInfo is highly interesting: it can return or change many many configuration settings from the 'Control Panel'. However it only returns the working area for the primary display monitor.
Functions RunningWindows98() and RunningWindows2000() are listed on page which version of Windows is running
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.
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.
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.
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.
Author: Todd G. Nist, Protech Systems Inc.
The source code for the window as shown in the picture is attached: w-disablex.w.
When you want to disable the [X]-button in the title bar and also want to remove the 'Close'-option from the system menu, you only have to call this function from within the main block of the window:
{windows.i} FUNCTION DisableWindowClose RETURNS LOGICAL ( /* parameter-definitions */ ) : /*--------------------------------------- Purpose: Notes: -----------------------------------------*/ DEFINE VARIABLE hSysMenu AS INTEGER NO-UNDO. DEFINE VARIABLE hParent AS INTEGER NO-UNDO. DEFINE VARIABLE hInstance AS INTEGER NO-UNDO. DEFINE VARIABLE iRetCode AS INTEGER NO-UNDO. DEFINE VARIABLE iCnt AS INTEGER NO-UNDO. RUN GetParent IN hpApi(INPUT {&window-name}:HWND, OUTPUT hParent). /* Get handle to the window's system menu (Restore, Maximize, Move, close etc.) */ RUN GetSystemMenu IN hpApi(INPUT hParent, INPUT 0, OUTPUT hSysMenu). IF hSysMenu NE 0 THEN DO: /* Get System menu's menu count */ RUN GetMenuItemCount IN hpApi(INPUT hSysMenu, OUTPUT iCnt). IF iCnt NE 0 THEN DO: /* Menu count is based on 0 (0, 1, 2, 3...) */ /* remove the "close option" */ RUN RemoveMenu IN hpApi(INPUT hSysMenu, INPUT iCnt - 1, INPUT {&MF_BYPOSITION} + {&MF_REMOVE}, OUTPUT iRetCode). /* remove the seperator */ RUN RemoveMenu IN hpApi(INPUT hSysMenu, INPUT iCnt - 2, INPUT {&MF_BYPOSITION} + {&MF_REMOVE}, OUTPUT iRetCode). /* Force caption bar's refresh which will disable the window close ("X") button */ RUN DrawMenuBar IN hpApi(INPUT hParent, OUTPUT iRetCode). {&window-name}:TITLE = "Try to close me!". END. /* if iCnt NE 0... */ END. /* if hSysMenu NE 0... */ RETURN FALSE. /* Function return value. */ END FUNCTION.
Because you now have restricted the user from closing the window you will have to close it yourself from within 4GL. This statement will do it:
apply "window-close" to {&window-name}.
There are many ways for doing things. To refresh the title bar, instead of running DrawMenuBar, you could also
run FlashWindow in hpApi(hParent,0, output iRetCode).
You can use this function when you want the taskbar button to flash.
run FlashTray. PROCEDURE FlashTray : DEFINE VARIABLE pfwi AS MEMPTR NO-UNDO. DEFINE VARIABLE hwndParent AS INTEGER NO-UNDO. RUN GetParent (CURRENT-WINDOW:HWND, OUTPUT hwndParent). SET-SIZE (pfwi) = 20. PUT-LONG (pfwi, 1) = GET-SIZE(pfwi). PUT-LONG (pfwi, 5) = hwndParent. PUT-LONG (pfwi, 9) = 2. /* = FLASW_TRAY */ PUT-LONG (pfwi,13) = 3. /* number of times to blink */ PUT-LONG (pfwi,17) = 0. /* blink rate in msec, 0=use system default */ RUN FlashWindowEx ( pfwi ). SET-SIZE(pfwi) = 0. END PROCEDURE. PROCEDURE GetParent EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER ipWindow AS LONG. DEFINE RETURN PARAMETER ipParent AS LONG. END PROCEDURE. PROCEDURE FlashWindowEx EXTERNAL "user32.dll": DEFINE INPUT PARAMETER ipWindow AS MEMPTR. END PROCEDURE.
Procedure GetDeviceCaps allows you to find many device capabilities, like: how many colors can be displayed, can a font be scaled or rotated, is the device capable of drawing lines or circles.
The "Device" can be the display but the procedure is also important for measuring the capabilities of a printer device, because your print routine may have to use different logic for penplotters or laserprinters or matrixprinters. Of course Windows can not actually measure the device but has to rely on the the device driver written by the manufacturer of the device.
You will use this procedure often together with GetSystemMetrics (to find out the dimensions of scrollbars or other objects) and with DeviceCapabilities (for printers only, to find info about paper sizes, orientation, number of bins and more)
A TrueColor picture can not be displayed when the display is configured for only 16 colors. At least not nicely. Suppose you have different sets of pictures for different display settings and now you want your program to decide which ones to use. You can do this by calling GetDeviceCaps, asking for the value on the BITSPIXEL index.
{windows.i} &GLOBAL-DEFINE BITSPIXEL 12 DEFINE VARIABLE ncolors AS INTEGER NO-UNDO. DEFINE VARIABLE txt AS CHARACTER NO-UNDO. RUN GetDisplayCaps( INPUT {&BITSPIXEL}, OUTPUT ncolors). CASE ncolors : WHEN 1 THEN txt = "16 colors". WHEN 8 THEN txt = "256 colors". WHEN 16 THEN txt = "High Color (16 bits per pixel)". WHEN 24 THEN txt = "True Color (24 bits per pixel)". WHEN 32 THEN txt = "True Color (32 bits per pixel)". OTHERWISE txt = "an unusual color depth!". END CASE. MESSAGE "your display is configured for " txt VIEW-AS ALERT-BOX.
Procedure GetDisplayCaps is a wrapper for the GetDeviceCaps and only works for the Display device. That's because the HDC (handle to device context) is used for a window. Any window will do, because all windows are on the same screen... This procedure uses the Progress DEFAULT-WINDOW because it always exists. If you want to find capabilities of a printer, you don't use GetDC but CreateDC.
PROCEDURE GetDisplayCaps : /* Wrapper for GetDeviceCaps when Device=display */ DEFINE INPUT PARAMETER cap-index AS INTEGER NO-UNDO. DEFINE OUTPUT PARAMETER capability AS INTEGER NO-UNDO. DEFINE VARIABLE hdc AS INTEGER NO-UNDO. DEFINE VARIABLE OK AS INTEGER NO-UNDO. RUN GetDC IN hpApi(INPUT DEFAULT-WINDOW:HWND, OUTPUT hdc). RUN GetDeviceCaps IN hpApi(INPUT hdc,INPUT cap-index, OUTPUT capability). RUN ReleaseDC IN hpApi(INPUT DEFAULT-WINDOW:HWND, INPUT hdc, OUTPUT OK). END PROCEDURE.
Warning:
Reparenting windows affects messaging. There is no way of knowing if this confuses the internals of the Progress runtime module. I would personally not dare to use something like this in a production environment.
One of the most Frequently Asked Questions is: "Every window has a taskbar button, how can I make those buttons invisible?"
The answer is that every unowned window or every window that does not have a parent has a taskbar button, except windows that have the 'toolwindow' style. So all you have to do is give your window a parent... easier said than done, because a parented window is normally glued to its parents client area and can't be moved away from that parent...
The solution was found by accident, it is actually a bug but is seems to work without negative side effects, as far as I can see. In fact, I see some cool side effects...
If you want window A to be parent of window B, you should normally use SetParent(B,A) but I accidently used SetWindowLong(B, -8, A). This was a silly mistake, nothing seemed to happen except the taskbar button for window B was gone!
I wonder if this is a bug or a feature. If it is a bug, it might not work in future Windows versions (mine is 95 with Service Pack 1).
By the way: I love taskbar buttons. I hate windows without taskbar buttons... but since it is a FAQ:
Suppose the project has one main window (a typical UIB-like menu tool) and this mainwin launches a couple of other windows. You only accept one taskbar button.
The main window uses this procedure to start a window:
{windows.i} {winfunc.i} ON CHOOSE OF btnLaunch IN FRAME DEFAULT-FRAME /* Start window without Taskbar button */ DO: DEFINE VARIABLE hp AS HANDLE NO-UNDO. DEFINE VARIABLE hWindow AS HANDLE NO-UNDO. DEFINE VARIABLE hOldParent AS INTEGER NO-UNDO. RUN noTaskBar.w PERSISTENT SET hp. RUN GetWindowHandle IN hp (OUTPUT hWindow). RUN SetWindowLongA IN hpApi (GetParent(hWindow:HWND), -8, GetParent({&WINDOW-NAME}:HWND), /* or DEFAULT-WINDOW:HWND */ OUTPUT hOldParent). END.
The launched window must have an internal procedure "GetWindowHandle" that returns the Progress widget handle for the newly created window:
PROCEDURE GetWindowHandle : DEFINE OUTPUT PARAMETER hWindow AS HANDLE NO-UNDO. hWindow = {&WINDOW-NAME}:HANDLE. END PROCEDURE.
As shown in the above example, you can parent the new window to
GetParent({&WINDOW-NAME}:HWND)
or to
DEFAULT-WINDOW:HWND
or to any other window. There is a strange but great bonus in the first choice:
The main window can not overlap the child windows. In a normal UIB-like application you get to see a lot of pieces of desktop and other underlying programs between your Progress windows, with the risc of activating one of them when you click one by accident.
Now you can safely maximize your main window - it hides the background but will never hide any other project windows. It has like a 'stay-on-bottom' effect.
If you minimize the mainwin, all other windows will be minimized too. If you restore the minimized mainwin, all open child windows will appear again.
If you use DEFAULT-WINDOW:HWND you have the benefit of an always available handle but the mainwin will not control the others.
When you close the parent window, all child windows seem to be closed as well (which is fine). I am not sure if Progress knows about that: if the child window procedure has an 'ON WINDOW-CLOSE' event handler it will not be called. To play safe, you should catch the 'close' in the parent procedure and notify all children.
If you use dynamic widgets, or if you dynamically resize or reposition widgets, you simply have to use function LockWindowUpdate especially on NT Terminal Server.
This is probably the most widely used API function, covered in every presentation and every publication. Actually that's why I didn't bother to cover LockWindowsUpdate before, but here it is at last...
PROCEDURE LockWindowUpdate EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER hWndLock AS LONG. DEFINE RETURN PARAMETER IsLocked AS LONG. END PROCEDURE.
LockWindowUpdate temporarily disables drawing in the specified window. While a window is locked you can change the appearance of the window or the appearance and/or positions of its child windows (widgets). These changes will not be drawn until the window is unlocked. When the window is unlocked its area will be invalidated and will eventually receive a WM_PAINT message. LockWindowUpdate will improve the overall performance of a drawing operation when you need to modify several widgets.
You should not move, resize, or hide/view the locked window while it has a lock. If you do you will see the desktop or other surrounding windows flash.
Only one window at a time can be locked.
If LockWindowUpdate failed (returned IsLocked=0) it may be because an other window owns the lock. This means you should not call LockWindowUpdate(0,..) if you didn't get the lock in the first place, because you may inadvertently unlock a different window.
Procedure LockWindowUpdate.w, which is attached, shows a window with a whole lot of widgets in it. When you press button "Move Widgets" each widget will be moved and resized a random amount of pixels. This operation is very slow and flashy if "use LockWindowUpdate" is not toggled.
The demo also shows the effect of hiding the frame during LockWindowUpdate: the window itself will behave quite nicely but all other visible windows, including the desktop will accidently be redrawn.
lockwindowupdate.w.zip : example source
Progress frames and windows show both scrollbars or none.
It would be better if the scrollbars were shown independently.
The procedure ShowScrollbarsWhenNeeded is to be called 'on end-size' or during initialize.
The procedure HideScrollbars does simply that: hide both scrollbars whether or not the virtual size is larger than the actual size. The procedure is mainly a demonstration of the {&SB_BOTH} constant.
{windows.i} PROCEDURE ShowScrollbarsWhenNeeded : /* purpose : to be called from "on end-size" or whenever. */ DEFINE INPUT PARAMETER hFrame AS HANDLE. DEFINE VARIABLE retval AS INTEGER NO-UNDO. IF hFrame:VIRTUAL-WIDTH-PIXELS > hFrame:WIDTH-PIXELS THEN RUN ShowScrollBar IN hpApi (hFrame:HWND, {&SB_HORZ}, -1, OUTPUT retval). ELSE RUN ShowScrollBar IN hpApi (hFrame:HWND, {&SB_HORZ}, 0, OUTPUT retval). IF hFrame:VIRTUAL-HEIGHT-PIXELS > hFrame:HEIGHT-PIXELS THEN RUN ShowScrollBar IN hpApi (hFrame:HWND, {&SB_VERT}, -1, OUTPUT retval). ELSE RUN ShowScrollBar IN hpApi (hFrame:HWND, {&SB_VERT}, 0, OUTPUT retval). END PROCEDURE. PROCEDURE HideScrollbars : /* purpose : hide both scrollbars */ DEFINE INPUT PARAMETER hFrame AS HANDLE. DEFINE VARIABLE retval AS INTEGER NO-UNDO. RUN ScrollUpperLeft(hFrame). RUN ShowScrollBar IN hpApi (hFrame:HWND, {&SB_BOTH}, 0, OUTPUT retval). END PROCEDURE. PROCEDURE ScrollUpperLeft : /* purpose : move both scrollbars to their 0% position */ DEFINE INPUT PARAMETER hFrame AS HANDLE. DEFINE VARIABLE wParam AS INTEGER NO-UNDO. DEFINE VARIABLE nPos AS INTEGER NO-UNDO. DEFINE VARIABLE RetVal AS INTEGER NO-UNDO. nPos = 0. wParam = (nPos * 256) + {&SB_THUMBPOSITION}. RUN SendMessage{&A} IN hpApi (hFrame:HWND, {&WM_HSCROLL}, wParam, 0, OUTPUT RetVal). RUN SendMessage{&A} IN hpApi (hFrame:HWND, {&WM_VSCROLL}, wParam, 0, OUTPUT RetVal). END PROCEDURE.
In procedure ScrollUpperleft, wParam has SB_THUMBPOSITION in its low byte to let windows know that you want to do something with the thumb position. The wanted position (nPos=0) is placed in the high byte by multiplying it by 256.
Frankly I only tried the value nPos=0 so I might as well have assigned
wParam = {&SB_THUMBPOSITION}.
For any other value for nPos you should test if the factor '256' is adequate for both 16-bits and 32-bits versions! (I guess not).
{windows.i} DEFINE VARIABLE RetVal AS INTEGER NO-UNDO. &GLOBAL-DEFINE SB_PAGEUP 2 &GLOBAL-DEFINE SB_PAGEDOWN 3 &GLOBAL-DEFINE SB_TOP 6 PROCEDURE ScrollUpperLeft : /* purpose : move both scrollbars to their 0% position */ DEFINE INPUT PARAMETER hFrame AS HANDLE. RUN SendMessage{&A} IN hpApi (hFrame:HWND, {&WM_HSCROLL}, {&SB_TOP}, 0, OUTPUT RetVal). RUN SendMessage{&A} IN hpApi (hFrame:HWND, {&WM_VSCROLL}, {&SB_TOP}, 0, OUTPUT RetVal). END PROCEDURE. PROCEDURE PageDown : DEFINE INPUT PARAMETER hFrame AS HANDLE. RUN SendMessage{&A} IN hpApi (hFrame:HWND, {&WM_VSCROLL}, {&SB_PAGEDOWN}, 0, OUTPUT RetVal). END PROCEDURE. PROCEDURE PageUp : DEFINE INPUT PARAMETER hFrame AS HANDLE. RUN SendMessage{&A} IN hpApi (hFrame:HWND, {&WM_VSCROLL}, {&SB_PAGEUP}, 0, OUTPUT RetVal). END PROCEDURE.
Ever tried to maximize a Progress window, with respect to the size and position of the Windows Taskbar? It is easy if you know about this not-well-documented feature (in 8.2A and up):
ASSIGN {&WINDOW-NAME}:MAX-HEIGHT = ? {&WINDOW-NAME}:MAX-WIDTH = ? {&WINDOW-NAME}:WINDOW-STATE = WINDOW-MAXIMIZED.
The fun part is assigning the unknown value to max-height/width. This results in dynamic resizing even when the user moves the Taskbar or changes the display resolution!
To restore a Window to its original state, use the following code:
ASSIGN {&WINDOW-NAME}:WINDOW-STATE = WINDOW-NORMAL.
To minimize a Window, use the following code:
ASSIGN {&WINDOW-NAME}:WINDOW-STATE = WINDOW-MINIMIZED.
by Rob den Boer
The attached example procedure creates a Progress window. When the window is minimized, it shows an icon in the system tray (that's in the corner of the taskbar, next to the clock). Features of this tray icon are:
* on left mouse click, the window is restored
* on right mouse click a popup-menu appears (as shown on picture)
* the icon has a tooltip
* the icon can be animated
Download systray.zip (12 kilobyte) and unzip it to a directory in your PROPATH.
The example was created using the AppBuilder in Progress 9, it will have to be rewritten to run in Progress 8. The example uses the PSTimer ActiveX control and also the MsgBlaster control.
Run procedure ip-systray-init when the window gets initialized.
This procedure takes three input parameters: a unique ID for the icon, a comma-separated list of .ico filenames and a tooltip string.
Procedure ip-systray-init shows the icon and sets the MsgBlaster OCX to pass mouse-messages from the icon on to the Progress procedure. The event-handler for the MsgBlaster contains the code for showing the popup-menu (on mouse-menu-click) or for restoring the window (on mouse-select-click).
The icon can be animated only by using a PSTimer control: on pstimer.tick simply run ip-next-icon. This procedure fetches the next name from a comma-separated list of .ico filenames and updates the taskbar icon. You may refine this procedure by using an Imagelist control.
Don't forget to call procedure ip-systray-exit when the program ends.
API-procedures used in this example are listed here to be included in the search index: PROCEDURE SHGetFileInfoA EXTERNAL "shell32" PROCEDURE Shell_NotifyIconA EXTERNAL "shell32" PROCEDURE SendMessageA EXTERNAL "user32"
More Progress examples available on the homepage of Rob den Boer: http:home.hccnet.nl/rc.den.boer/progress/index.html
systray.zip : (by Rob den Boer, improved by Peter Kiss)
Sometimes you may want to do some extra painting on a Progress frame or window, for example some circles, dotted lines, dashed area's or just lines that are not horizontal or vertical. Or how about right-aligned text?
The actual drawing can be done with GDI functions, but the result will be erased whenever Progress repaints the frame (or window). This topic is about preventing that.
Let's start with a simple example: drawing an ellipse on a Progress frame.
create a Progress window (doesn't have to be Smart) and place a button on it. On choose of this button: run Paint.
PROCEDURE PAINT : /*---------------------------------------------------- Purpose: do some custom painting, in this case: draw a circle as large as the window ------------------------------------------------------ */ DEFINE VARIABLE hdc AS INTEGER NO-UNDO. DEFINE VARIABLE Okay AS INTEGER NO-UNDO. RUN GetDC IN hpApi (INPUT FRAME {&frame-name}:HWND, OUTPUT hdc). RUN Ellipse IN hpApi (hdc, 0, 0, FRAME {&frame-name}:WIDTH-PIXELS, FRAME {&frame-name}:HEIGHT-PIXELS, OUTPUT Okay ). RUN ReleaseDC IN hpApi (INPUT FRAME {&frame-name}:HWND, INPUT hdc, OUTPUT Okay). END PROCEDURE.
Now when you run the window and press the button you will see a large ellipse. When you drag any other window over the surface of your window, you will see that overlapped regions of the ellipse will be erased. To repaint the ellipse you can just press the button again, but you want a way to do this automatically.
A (region of a) window will be painted again when it has been overlapped by another window, or whenever the (region of the) window has become invalidated. MS-Windows sends a series of messages to the window when it is invalidated and Progress responds to it by painting the region again. Among those messages are WM_ERASEBKGND and WM_PAINT.
I have always believed that WM_PAINT was the proper message to wait for, but there are occasions when you see that progress repaints the window without ever having trapped a WM_PAINT message. Result: your custom drawing is erased.
Matt Gilarde at PSC Development explains what's going on:
Progress doesn't repaint a frame when it gets a WM_PAINT message; we do it when we get a WM_ERASEBKGND. Why? I believe the idea was to avoid flashing during repaints. Instead of wiping out the background in the WM_ERASEBKGND and then repainting widgets in the WM_PAINT, Progress does all the painting during WM_ERASEBKGND. Painting a frame consists of the following steps:
* Fill the frame with the background color
* Draw the grid if it is on
* Paint rectangles and images
* Paint all other widgets
* Highlight selected widgets
Since the painting is handled in WM_ERASEBKGND, the WM_PAINT message is not always generated (Windows removes the WM_PAINT from the message queue if there is no invalid region to be painted). So you may have better luck trapping WM_ERASEBKGND. Or you may run into other problems.
So you will have to set up your MessageBlaster to trap WM_ERASEBKGND instead WM_PAINT.
Let's do it:
Drop a MessageBlaster ActiveX control on your window and set it up as follows:
PROCEDURE CtrlFrame.Msgblst32.MESSAGE . DEFINE INPUT PARAMETER p-MsgVal AS INTEGER NO-UNDO. DEFINE INPUT PARAMETER p-wParam AS INTEGER NO-UNDO. DEFINE INPUT PARAMETER p-lParam AS INTEGER NO-UNDO. DEFINE INPUT-OUTPUT PARAMETER p-lplRetVal AS INTEGER NO-UNDO. CASE p-MsgVal : WHEN 20 /* = WM_ERASEBKGND */ THEN RUN paint. END CASE. END PROCEDURE. PROCEDURE initialize-controls : /*------------------------------------------------------------------------------ Purpose: listen for WM_ERASEBKGND messages ------------------------------------------------------------------------------*/ chCtrlFrame:Msgblst32:MsgList(0) = 20. /* = WM_ERASEBKGND */ chCtrlFrame:Msgblst32:MsgPassage(0) = -1. /* = let PSC handle the message first */ chCtrlFrame:Msgblst32:hWndTarget = FRAME {&frame-name}:HWND. END PROCEDURE.
If you want Progress to repaint the entire frame (or window), for example to erase your custom painting, you can not just send it a WM_ERASEBKGND message. Here's another excellent explanation from Matt Gilarde:
Sending WM_ERASEBKGND won't cause any repainting to be done since it requires the wParam to be the handle to a device context for the area which is to be repainted. To force a repaint, you need to invalidate all or part of the window or frame. Windows will then generate the proper WM_ERASEBKGND and WM_PAINT messages. You can use the InvalidateRect() API to invalidate a window.
BOOL InvalidateRect(HWND hWnd, RECT *lpRect, BOOL bErase);
Calling InvalidateRect(hWnd, 0, 1) will force the entire window to repaint. You can call UpdateWindow(hWnd) to force the repaint to occur immediately (otherwise the paint messages will sit in the message queue until Progress gets around to looking for them). The result may not be what you want, however, since there may be lots of flashing when you force the repaint.
This example uses the source in procedure winstyle.p, available on page WinStyle.
A normal window can be maximized to full-screen but a Progress window doesn't grow much unless you specifically set virtual sizes. Maximizing (or resizing at all) isn't very useful for a window that has a fixed amount of widgets, so you might prefer to hide the Maximize button. And while we are at it let's also hide the Minimize button.
Here's how you do it:
Create a Progress window and place this code fragment somewhere in the source:
ASSIGN {&window-name} :MIN-BUTTON = NO {&window-name} :MAX-BUTTON = NO.
Let's take a look at the source in winstyle.p.
The title bar is on the NonClient-area of the window, but the input parameter {&window-name}:hWnd is the Client-area. So the first task is to find the hWnd of the NonClient-area, this is simply done by repeatedly calling GetParent until a window is found that owns a caption. This method allows that the input parameter can be any widget:hWnd.
Now the style flags are fetched from the already existing NonClient-window, some style bits are set to zero and the new style is pushed back into the window. The min/max buttons are now invalid so I figured it's reasonable to make resizing of the window impossible.
The standard frame of type WS_THICKFRAME is sensitive for the mouse; users can drag the frame to resize it, so I also deleted the WS_THICKFRAME style. This means you now get a frame that is slightly less thick. Because of that you get to see a narrow transparent area between the new thin frame and the Client-area. This must be solved by shrinking the NonClient window until it tightly fits around the Client window.
The api function AdjustWindowRect is designed to calculate the required size of a window, given a certain size for the Client-area. So that's what we call. The size of the client-area is found by calling GetClientRect.
The new dimensions for the NonClient window are assigned by the SetWindowPos function.
We're done!
The menu-items in the system-menu for 'Size', 'Minimize' and 'Maximize' are automatically disabled as a result of the new window style. Disabled menu-items imply they may become enabled, so I decided to delete them.
Splash is the name for a window that is shown during startup of an application. It masks a long loading time (for example during establishing connections to remote databases) and is often used to show the application title, 'licensed to'-info, author name and often a nice picture.
A Splash has no user interaction; a user must simply wait until the show begins. Therefore a Splash should not have a title bar and especially no 'close' button. It's common to display the Splash in the exact center of the screen and it also usually 'stays-on-top'.
Most people use third-party 3GL languages to create a Splash screen because it seems impossible to create one in Progress. The downside of using an external program is that it's hard to determine the proper time to close. This would be lots easier if the Splash was created in Progress and then it would also be possible to give status information (like "now connecting to system.db").
The next procedure accepts the hWnd of a Progress window (not a Frame or Dialog!) and makes it look like a Splash by removing the title bar and the thick frame, centering it to the screen and making it topmost.
/* =================================================================== file : MkSplash.p by : Jurjen Dijkstra, 1997 language : Progress 8.2A on Windows 95 purpose : changes the appearance of a normal Progress window into a Splash window, e.g. no caption, no border, centered to screen, stay-on-top. params : hClient = HWND of a client window ThinBorder = YES if a WS_BORDER style is wanted NO creates no border at all usage : during mainblock: run MkSplash.p ({&WINDOW-NAME}:HWND, YES). =================================================================== */ DEFINE INPUT PARAMETER hClient AS INTEGER. DEFINE INPUT PARAMETER ThinBorder AS LOGICAL. {windows.i} {ProExtra.i} DEFINE VARIABLE hNonclient AS INTEGER NO-UNDO. DEFINE VARIABLE style AS INTEGER NO-UNDO. DEFINE VARIABLE oldstyle AS INTEGER NO-UNDO. hNonclient = GetParent(hClient). /* delete the caption and the thickframe */ RUN GetWindowLong{&A} IN hpApi(hNonclient, {&GWL_STYLE}, OUTPUT style). RUN Bit_Remove IN hpExtra(INPUT-OUTPUT style, {&WS_CAPTION}). RUN Bit_Remove IN hpExtra(INPUT-OUTPUT style, {&WS_THICKFRAME}). RUN SetWindowLong{&A} IN hpApi(hNonclient, {&GWL_STYLE}, style, OUTPUT oldstyle). /* the next block creates a thin border around the window. This has to be done in a second SetWindowLong */ IF ThinBorder THEN DO: RUN GetWindowLong{&A} IN hpApi(hNonclient, {&GWL_STYLE}, OUTPUT style). RUN Bit_Or IN hpExtra(INPUT-OUTPUT style, {&WS_BORDER}). RUN SetWindowLong{&A} IN hpApi(hNonclient, {&GWL_STYLE}, style, OUTPUT oldstyle). END. /* The above changes in window styles are usually done before the window is created. Now we are actually too late, windows will not respond with an automatic redraw of the window. We will have to force it. This is done by calling SetWindowPos with the SWP_FRAMECHANGED flag. Since we are calling SetWindowPos we might as well ask it to perform some other actions, like: make this a TOPMOST window, change the coordinates (centered to screen) */ DEFINE VARIABLE lpRect AS MEMPTR NO-UNDO. DEFINE VARIABLE WIDTH AS INTEGER NO-UNDO. DEFINE VARIABLE HEIGHT AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. /* the lpRect structure is defined as LEFT,TOP,RIGHT,BOTTOM. */ SET-SIZE(lpRect) = 4 * {&INTSIZE}. /* get the dimensions of the client area: */ RUN GetWindowRect IN hpApi(hClient, GET-POINTER-VALUE(lpRect), OUTPUT ReturnValue). /* let Windows calculate how large the NonClient area must be to fit exactly around the Client area: */ RUN AdjustWindowRect IN hpApi(GET-POINTER-VALUE(lpRect), style, 0, OUTPUT ReturnValue). /* so these will be the new dimensions of the Nonclient area: */ WIDTH = get-{&INT}(lpRect, 1 + 2 * {&INTSIZE}) - get-{&INT}(lpRect, 1 + 0 * {&INTSIZE}). HEIGHT = get-{&INT}(lpRect, 1 + 3 * {&INTSIZE}) - get-{&INT}(lpRect, 1 + 1 * {&INTSIZE}). SET-SIZE(lpRect) = 0. /* Do it. SWP_FRAMECHANGED is the most important flag here */ RUN SetWindowPos IN hpApi (hNonclient, -1, /* = HWND_TOPMOST */ INTEGER((SESSION:WIDTH-PIXELS - WIDTH) / 2), INTEGER((SESSION:HEIGHT-PIXELS - HEIGHT) / 2), WIDTH, HEIGHT, {&SWP_NOACTIVATE} + {&SWP_FRAMECHANGED}, OUTPUT ReturnValue ). RETURN.
The call to AdjustWindowRect assumes the window has no menu.
When centering the window I should have taken the visibility, position and size of Taskbar(s) into account. That would not have been too hard, see center a window to the working area
Parameter ThinBorder=NO causes the window to have no border at all. This looks nice if the Splash window is 100% covered with a picture that has its own edges.
Zane Appel created an example Splash window, based on the mksplash.p procedure. This splash window will disappear after 5 seconds or when the user clicks anywhere on the splash window. The example is attached, see splashdm.zip
splashdm.zip : demo by Zane Appel, based on mkspash.p
In Windows 2000 you can make transparent windows, by using function SetLayeredWindowAttributes. The attached Progress procedure demonstrates this.
transparent.w.zip : demo SetLayeredWindowAttributes
.
The MsgBlaster or MessageBlaster is a freeware ActiveX control that helps you catch every Windows message. This control is used in several examples in this Win32 zone, see topic context-help for a practical example.
You can download the MsgBlaster from: http:extra.newsguy.com/~bridge/mbocx32b.zip
ProExtra.dll is a small Delphi utility I wrote to call some low-level stuff that would otherwise be hard to do from within 4GL.
proextra.dll and its Delphi source are included in "everything.zip" which is available for download on page windows.i and hpApi.
The Win32 API contains many constants.
winconst.zip contains a list of more than 14000 P4GL preprocessor definitions and a utility that helps to find dependencies.
For example, the constant WVR_REDRAW is defined as (WVR_HREDRAW | WVR_VREDRAW).
When you search for WVR_REDRAW in winconst.exe it will return
&GLOBAL-DEFINE WVR_VREDRAW 512 &GLOBAL-DEFINE WVR_HREDRAW 256 &GLOBAL-DEFINE WVR_REDRAW ({&WVR_HREDRAW} + {&WVR_VREDRAW})
winconst.zip is freeware. So feel free to download it :-)
You have to be careful when you combine constants. In 3GL programming environments you would use the OR operator to combine constants, but in Progress we only have + to work with. This can make a difference. Also, we have no unsigned long integer. As a result, some values are converted to negative signed integers. Using these values together with the + operator instead of OR may give unexpected results.
----
Jeff Pilant writes on 11 Januari 2002:
I just saw a post on peg@peg.com about this site: http:www.other-coast.com/
It pointed out how to grab constants directly from the com object file.
The page http:home.attbi.com/~marc.lafleur/Articles/ExtractingNamedConstants.html talks about a microsoft provided program to do this.
http:www.microsoft.com/com/resources/OVI386.EXE and his own program to convert this to 4GL
http:home.attbi.com/~marc.lafleur/External/typedef.zip along with samples for a number of MS products in http:home.attbi.com/~marc.lafleur/Articles/OfficeIncludes.html
26 Nov 2002: many constants for MS-Access added by Jeff Pilant
Throughout the site you will see references to windows.i and the handle hpApi.
hpApi is defined in windows.i and is a procedure-handle to windows.p.
windows.p contains a bunch of API procedures.
You can get a copy of windows.i and windows.p: they are in "everything.zip" which also contains some more API-related sources .
everything.zip : contains windows.i and windows.p, and more :-)
You can find procedure winstyle.p in everything.zip, available on page windows.i and hpApi
Some articles about how to send, or read e-mail from within Progress
This solution is made by Johann van der Merwe.
It allows one attachment.
{windows.i} RUN mapi ( "you@work.com", "yourwife@home.com", "late for dinner", "something came up..", "c:\images\flowers.bmp" ). /* ========================================================================== */ PROCEDURE MAPI : DEFINE INPUT PARAMETER OriginName AS CHARACTER. DEFINE INPUT PARAMETER RecipName AS CHARACTER. DEFINE INPUT PARAMETER Subject AS CHARACTER. DEFINE INPUT PARAMETER Bodytext AS CHARACTER. DEFINE INPUT PARAMETER FilePathName AS CHARACTER. DEFINE VARIABLE SubjPtr AS MEMPTR. SET-SIZE(SubjPtr) = LENGTH(Subject) + 1. /* maximum = 255 */ PUT-STRING(SubjPtr,1) = Subject. DEFINE VARIABLE TextPtr AS MEMPTR. SET-SIZE(TextPtr) = 16000. PUT-STRING(TextPtr,1) = Bodytext + (IF FilePathName = "":U THEN "":U ELSE CHR(10) + CHR(10) + " ":U). /* if file attached place at end of Bodytext with line skip */ /* ---------------- Build Originator details ------------------------ */ DEFINE VARIABLE OriginNamePtr AS MEMPTR. SET-SIZE(OriginNamePtr) = LENGTH(OriginName) + 1. /* maximum = 255 */ PUT-STRING(OriginNamePtr,1) = OriginName. /* Originator name */ DEFINE VARIABLE OriginDescPtr AS MEMPTR. SET-SIZE(OriginDescPtr) = 24. PUT-LONG(OriginDescPtr,1) = 0. /* Reserved */ PUT-LONG(OriginDescPtr,5) = 0. /* RecipClass 0 = MAPI_ORIG */ PUT-LONG(OriginDescPtr,9) = GET-POINTER-VALUE(OriginNamePtr). /* Name */ PUT-LONG(OriginDescPtr,13) = 0. /* Address */ PUT-LONG(OriginDescPtr,17) = 0. /* EID Size */ PUT-LONG(OriginDescPtr,21) = 0. /* Entry ID */ /* ----------------Build Recipient details -------------------------- */ DEFINE VARIABLE RecipNamePtr AS MEMPTR. SET-SIZE(RecipNamePtr) = LENGTH(RecipName) + 1./* maximum = 255 */ PUT-STRING(RecipNamePtr,1) = RecipName. /* Recipient name */ DEFINE VARIABLE RecipDescPtr AS MEMPTR. SET-SIZE(RecipDescPtr) = 24. PUT-LONG(RecipDescPtr,1) = 0. /* Reserved */ PUT-LONG(RecipDescPtr,5) = 1. /* RecipClass 1 = MAPI_TO */ PUT-LONG(RecipDescPtr,9) = GET-POINTER-VALUE(RecipNamePtr). /* Name */ PUT-LONG(RecipDescPtr,13) = 0. /* Address */ PUT-LONG(RecipDescPtr,17) = 0. /* EID Size */ PUT-LONG(RecipDescPtr,21) = 0. /* Entry ID */ /* --------------- Build File Details ------------------- */ IF FilePathName <> "":U THEN DO: DEFINE VARIABLE FilePathNamePtr AS MEMPTR. SET-SIZE(FilePathNamePtr) = LENGTH(FilePathName) + 1. /* maximum = 255 */ PUT-STRING(FilePathNamePtr,1) = FilePathName. /* File pathname */ DEFINE VARIABLE FILENAME AS CHARACTER NO-UNDO. FILENAME = SUBSTRING(FilePathName,R-INDEX(FilePathName,"\":U) + 1). /* extract filename (starting on last \) from filefullname */ FILENAME = " ":U + FILENAME. /* for some strange reason the first five chars disappear */ DEFINE VARIABLE FileNamePtr AS MEMPTR. SET-SIZE(FileNamePtr) = LENGTH(FILENAME) + 1. /* maximum = 255 */ PUT-STRING(FileNamePtr,1) = FILENAME. /* File name */ DEFINE VARIABLE FileDescPtr AS MEMPTR. SET-SIZE(FileDescPtr) = 24. PUT-LONG(FileDescPtr,1) = 0. /* Reserved */ PUT-LONG(FileDescPtr,5) = 0. /* Flags 0 = data file */ PUT-LONG(FileDescPtr,9) = LENGTH(Bodytext) + 2. /* Position */ PUT-LONG(FileDescPtr,13) = GET-POINTER-VALUE(FilePathNamePtr). /* PathName */ PUT-LONG(FileDescPtr,17) = GET-POINTER-VALUE(FileNamePtr). /* FileName */ PUT-LONG(FileDescPtr,21) = 0. /* FileType */ END. /* ---------- Build Message Details ------------------- */ DEFINE VARIABLE MessageDescPtr AS MEMPTR. SET-SIZE(MessageDescPtr) = 48. PUT-LONG(MessageDescPtr,1) = 0. /* Reserved */ PUT-LONG(MessageDescPtr,5) = GET-POINTER-VALUE(SubjPtr). /* Subject */ PUT-LONG(MessageDescPtr,9) = GET-POINTER-VALUE(TextPtr). /* Text */ PUT-LONG(MessageDescPtr,13) = 0. /* MessageType */ PUT-LONG(MessageDescPtr,17) = 0. /* DateReceived */ PUT-LONG(MessageDescPtr,21) = 0. /* ConversationID */ PUT-LONG(MessageDescPtr,25) = 1. /* Flags */ PUT-LONG(MessageDescPtr,29) = GET-POINTER-VALUE(OriginDescPtr). /* Originator */ PUT-LONG(MessageDescPtr,33) = 1. /* RecipCount */ PUT-LONG(MessageDescPtr,37) = GET-POINTER-VALUE(RecipDescPtr). /* Recips */ PUT-LONG(MessageDescPtr,41) = (IF FilePathName = "":U THEN 0 ELSE 1). /* FileCount */ PUT-LONG(MessageDescPtr,45) = (IF FilePathName = "":U THEN 0 ELSE GET-POINTER-VALUE(FileDescPtr)). /* Files */ /* EO Build Message Details */ /* -------- Send Message ------------ */ DEFINE VARIABLE ResultInt AS INTEGER NO-UNDO. RUN MAPISendMail IN hpApi (INPUT 0, INPUT 0, INPUT GET-POINTER-VALUE(MessageDescPtr), INPUT 11, /* 1 = MAPI_LOGON_UI + 2 = MAPI_NEW_SESSION + 8 = MAPI_DIALOG */ INPUT 0, OUTPUT ResultInt). RUN MapiReturnCode (ResultInt). /* ------- Free memory ------------ */ SET-SIZE(SubjPtr) = 0. SET-SIZE(TextPtr) = 0. SET-SIZE(OriginNamePtr) = 0. SET-SIZE(OriginDescPtr) = 0. SET-SIZE(RecipNamePtr) = 0. SET-SIZE(RecipDescPtr) = 0. SET-SIZE(FilePathNamePtr) = 0. SET-SIZE(FileNamePtr) = 0. END PROCEDURE.
=
This solution is by Ian Richards. A COM solution is easier, but this allows me to send automated e-mails from a server where Outlook is not installed. The recipients and attachments are presented as comma separated lists.
{windows.i} RUN mapimulti( INPUT "you@originator.com", INPUT "recipient1@address1.com,recipient2@address2.com", INPUT "Message Title", INPUT "Message Text", INPUT "C:\file1.txt,C:\file2.txt"). /* ========================================================================== */ PROCEDURE mapimulti: {windows.i} DEFINE INPUT PARAMETER ipOrigName AS CHARACTER NO-UNDO. /* Originator Name */ DEFINE INPUT PARAMETER ipRecipName AS CHARACTER NO-UNDO. /* Recipient(s) names, comma separated */ DEFINE INPUT PARAMETER ipSubject AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER ipBodytext AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER ipFilePathName AS CHARACTER NO-UNDO. /* Attachment(s) names, comma separated */ DEFINE VARIABLE intRecips AS INTEGER NO-UNDO. /* Count Recipients */ DEFINE VARIABLE intAttachs AS INTEGER NO-UNDO. /* Count Attachments */ DEFINE VARIABLE RecipName AS CHARACTER EXTENT 20 NO-UNDO. /* Recipient Array */ DEFINE VARIABLE FilePathName AS CHARACTER EXTENT 20 NO-UNDO. /* Attachment Array */ /* Memptr Definitions */ DEFINE VARIABLE SubjPtr AS MEMPTR NO-UNDO. /* Message Subject */ DEFINE VARIABLE TextPtr AS MEMPTR NO-UNDO. /* Message Text */ DEFINE VARIABLE OriginNamePtr AS MEMPTR. /* Originator Name */ DEFINE VARIABLE RecipNamePtr AS MEMPTR EXTENT 20 NO-UNDO. /* Array of pointers to Recipient Name */ DEFINE VARIABLE RecipDescPtr AS MEMPTR EXTENT 20 NO-UNDO. /* Array of pointers to Recipient Description */ DEFINE VARIABLE FilePathNamePtr AS MEMPTR EXTENT 20 NO-UNDO. /* Array of pointers to Attachment Path */ DEFINE VARIABLE FileNam AS CHARACTER EXTENT 20 NO-UNDO. /* Array of Attachment Name */ DEFINE VARIABLE FileNamPtr AS MEMPTR EXTENT 20 NO-UNDO. /* Array of pointers to Attachment Name */ DEFINE VARIABLE FileDescPtr AS MEMPTR EXTENT 20 NO-UNDO. /* Array of pointers to Attachment Description */ DEFINE VARIABLE MessageDescPtr AS MEMPTR NO-UNDO. /* Pointer To Message Structure */ DEFINE VARIABLE FileArrayPtr AS MEMPTR NO-UNDO. /* Pointer to Array of File Decription */ DEFINE VARIABLE RecipArrayPtr AS MEMPTR NO-UNDO. /* Pointer to Array of Recipient */ DEFINE VARIABLE zz AS INTEGER NO-UNDO. /* General Purpose Integer */ /* --- POPULATE RECIPIENT & ATTACHMENT ARRAYS -------*/ RUN recips. /* Populate RecipName */ RUN attachs. /* Populate FilePath Name */ /* --- SET MESSAGE TEXT AND SUBJECT -----------------*/ SET-SIZE(SubjPtr) = LENGTH(ipSubject) + 1. /* maximum = 255 */ PUT-STRING(SubjPtr,1) = ipSubject. SET-SIZE(TextPtr) = 16000. PUT-STRING(TextPtr,1) = ipBodytext. /* --- BUILD ORIGINATOR DETAILS ---------------------*/ SET-SIZE(OriginNamePtr) = LENGTH(ipOrigName) + 1. /* maximum = 255 */ PUT-STRING(OriginNamePtr,1) = ipOrigName. /* Originator name */ DEFINE VARIABLE OriginDescPtr AS MEMPTR. SET-SIZE(OriginDescPtr) = 24. PUT-LONG(OriginDescPtr,1) = 0. /* Reserved */ PUT-LONG(OriginDescPtr,5) = 0. /* RecipClass 0 = MAPI_ORIG */ PUT-LONG(OriginDescPtr,9) = GET-POINTER-VALUE(OriginNamePtr). /* Name */ PUT-LONG(OriginDescPtr,13) = 0. /* Address */ PUT-LONG(OriginDescPtr,17) = 0. /* EID Size */ PUT-LONG(OriginDescPtr,21) = 0. /* Entry ID */ /* ---------- BUILD RECIPIENT DETAILS -------------- */ DO zz = 1 TO intRecips : SET-SIZE(RecipNamePtr[zz]) = LENGTH(RecipName[zz]) + 1. /* maximum = 255 */ PUT-STRING(RecipNamePtr[zz],1) = RecipName[zz]. /* Recipient name */ SET-SIZE(RecipDescPtr[zz]) = 24. PUT-LONG(RecipDescPtr[zz],1) = 0. /* Reserved */ PUT-LONG(RecipDescPtr[zz],5) = 1. /* RecipClass 1 = MAPI_TO */ PUT-LONG(RecipDescPtr[zz],9) = GET-POINTER-VALUE(RecipNamePtr[zz]). /* Name */ PUT-LONG(RecipDescPtr[zz],13) = 0. /* Address */ PUT-LONG(RecipDescPtr[zz],17) = 0. /* EID Size */ PUT-LONG(RecipDescPtr[zz],21) = 0. /* Entry ID */ END. /* Populate Memory Indicated By RecipArrayPtr */ SET-SIZE(RecipArrayPtr) = 24 * intRecips. DO zz = 1 TO intRecips : PUT-BYTES(RecipArrayPtr, (zz * 24) - 23) = GET-BYTES(RecipDescPtr[zz],1,24). END. /* ---------- BUILD FILE DETAILS ------------------- */ /* Build File Description Array */ DO zz = 1 TO intAttachs: SET-SIZE(FilePathNamePtr[zz]) = LENGTH(FilePathName[zz]) + 1. /* maximum = 255 */ PUT-STRING(FilePathNamePtr[zz],1) = FilePathName[zz]. /* File pathname */ FileNam[zz] = SUBSTRING(FilePathName[zz],R-INDEX(FilePathName[zz],"\":U) + 1). SET-SIZE(FileNamPtr[zz]) = LENGTH(FileNam[zz]) + 1. /* maximum = 255 */ PUT-STRING(FileNamPtr[zz],1) = FileNam[zz]. /* File name */ SET-SIZE(FileDescPtr[zz]) = 24. PUT-LONG(FileDescPtr[zz],1) = 0. /* Reserved */ PUT-LONG(FileDescPtr[zz],5) = 0. /* Flags 0 = data file */ PUT-LONG(FileDescPtr[zz],9) = -1. /* Position */ PUT-LONG(FileDescPtr[zz],13) = GET-POINTER-VALUE(FilePathNamePtr[zz]). /* PathName */ PUT-LONG(FileDescPtr[zz],17) = GET-POINTER-VALUE(FileNamPtr[zz]). /* File Name */ PUT-LONG(FileDescPtr[zz],21) = 0. /* FileType */ END. /* Populate Memory Indicated By FileArrayPtr */ SET-SIZE(FileArrayPtr) = 24 * intAttachs. DO zz = 1 TO intAttachs: PUT-BYTES(FileArrayPtr, (zz * 24) - 23) = GET-BYTES(FileDescPtr[zz],1,24). END. /* ---------- BUILD MESSAGE DETAILS ---------------- */ SET-SIZE(MessageDescPtr) = 48. PUT-LONG(MessageDescPtr,1) = 0. /* Reserved */ PUT-LONG(MessageDescPtr,5) = GET-POINTER-VALUE(SubjPtr). /* Subject */ PUT-LONG(MessageDescPtr,9) = GET-POINTER-VALUE(TextPtr). /* Text */ PUT-LONG(MessageDescPtr,13) = 0. /* MessageType */ PUT-LONG(MessageDescPtr,17) = 0. /* DateReceived */ PUT-LONG(MessageDescPtr,21) = 0. /* ConversationID */ PUT-LONG(MessageDescPtr,25) = 1. /* Flags */ PUT-LONG(MessageDescPtr,29) = GET-POINTER-VALUE(OriginDescPtr). /* Originator */ PUT-LONG(MessageDescPtr,33) = intRecips. /* RecipCount */ PUT-LONG(MessageDescPtr,37) = GET-POINTER-VALUE(RecipArrayPtr). /* Recips */ PUT-LONG(MessageDescPtr,41) = intAttachs. /* FileCount */ PUT-LONG(MessageDescPtr,45) = GET-POINTER-VALUE(FileArrayPtr). /* Files */ /* ---------- SEND MESSAGE ------------------------- */ DEFINE VARIABLE ResultInt AS INTEGER NO-UNDO. RUN MAPISendMail IN hpApi (INPUT 0, INPUT 0, INPUT GET-POINTER-VALUE(MessageDescPtr), INPUT 0, /* 1 = MAPI_LOGON_UI + 2 = MAPI_NEW_SESSION + 8 = MAPI_DIALOG */ INPUT 0, OUTPUT ResultInt). IF ResultInt <> 0 THEN RUN MapiReturnCode.p (INPUT ResultInt). /* ---------- RELEASE RESOURCES -------------------- */ SET-SIZE(SubjPtr) = 0. SET-SIZE(TextPtr) = 0. DO zz = 1 TO intAttachs : SET-SIZE(FilePathNamePtr[zz]) = 0. SET-SIZE(FileNamPtr[zz]) = 0. SET-SIZE(FilePathNamePtr[zz]) = 0. SET-SIZE(FileDescPtr[zz]) = 0. END. DO zz = 1 TO intRecips : SET-SIZE(RecipNamePtr[zz]) = 0. SET-SIZE(RecipDescPtr[zz]) = 0. END. SET-SIZE(MessageDescPtr) = 0. SET-SIZE(FileArrayPtr) = 0. SET-SIZE(RecipArrayPtr) = 0. SET-SIZE(OriginNamePtr) = 0. IF VALID-HANDLE(hpAPI) THEN DELETE OBJECT hpAPI. /* ---------- INTERNAL PROCEDURES ------------------ */ PROCEDURE recips: /* --------------------------------------------*/ /* Populate Reciptient Array, RecipName, */ /* from comma separated list. */ /*---------------------------------------------*/ DEFINE VARIABLE xx AS INTEGER NO-UNDO. IntRecips = 0. DO WHILE ipRecipName <> "": xx = INDEX(ipRecipName, ","). IF xx > 0 THEN DO: intRecips = intRecips + 1. RecipName[intRecips] = SUBSTRING(ipRecipName, 1, xx - 1). ipRecipName = SUBSTRING(ipRecipName, xx + 1). END. ELSE DO: IF ipRecipName <> "" THEN DO: intRecips = intRecips + 1. RecipName[intRecips] = ipRecipName. ipRecipName = "". END. END. END. END PROCEDURE. PROCEDURE attachs: /* --------------------------------------------*/ /* Populate Attachment Array, FilePathName */ /* from comma separated list. */ /*---------------------------------------------*/ DEFINE VARIABLE yy AS INTEGER NO-UNDO. IntAttachs = 0. DO WHILE ipFilePathName <> "": yy = INDEX(ipFilePathName, ","). IF yy > 0 THEN DO: intAttachs = intAttachs + 1. FilePathName[intAttachs] = TRIM(SUBSTRING(ipFilePathName, 1, yy - 1)). ipFilePathName = TRIM(SUBSTRING(ipFilePathName, yy + 1)). END. ELSE DO: IF ipFilePathName <> "" THEN DO: intAttachs = intAttachs + 1. FilePathName[intAttachs] = TRIM(ipFilePathName). ipFilePathName = "". END. END. END. END PROCEDURE. END PROCEDURE.
The Tapi (Telephony API) can be used to dial a telephone number or pager.
This source is from Johann van der Merwe.
His introduction says:
This is an example of what we use. The code is not cleaned up. We also
use our own dialing properties (for instance a outside line). Hope this
helps. Use MSDN to look up tapiRequestMakeCall for parameter info.
DEFI: DO ON ERROR UNDO, RETURN "ERROR-DEFI":U: DEFINE INPUT PARAMETER TelNoStr AS CHARACTER NO-UNDO. DEFINE VARIABLE GetLine AS CHARACTER NO-UNDO. DEFINE VARIABLE TelNo AS MEMPTR NO-UNDO. DEFINE VARIABLE i AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. END. Main: DO ON ERROR UNDO, RETURN "ERROR-MAIN":U: GET-KEY-VALUE SECTION "Modem":U KEY "GetLine":U VALUE GetLine. IF GetLine = "?":U OR GetLine = ? THEN GetLine = "":U. SET-SIZE(TelNo) = LENGTH(GetLine + TelNoStr) + 1. DO i = 1 TO LENGTH(TelNoStr + GetLine): PUT-BYTE(TelNo,i) = ASC(SUBSTRING(GetLine + TelNoStr,i,1)). END. PUT-BYTE(TelNo,i) = 0. RUN tapiRequestMakeCall (INPUT GET-POINTER-VALUE(TelNo), INPUT "0":U, INPUT "0":U, INPUT "0":U, OUTPUT ReturnValue). END. PROCEDURE tapiRequestMakeCall EXTERNAL "tapi32.dll":U: DEFINE INPUT PARAMETER lpszDestAddress AS LONG. DEFINE INPUT PARAMETER lpszAppName AS CHARACTER. DEFINE INPUT PARAMETER lpszCalledParty AS CHARACTER. DEFINE INPUT PARAMETER lpszComment AS CHARACTER. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.
There are different kinds of solutions for using Email from Progress. For example, there are several ActiveX components, there is a Progress 7+8 product with source made by Ketil Parow (you can download a trial version at http:www.sn.no/~ketilp/misc/mapi.zip), you can use the MAPI OLE automation server (there is a document about that on the ActiveX pages on www.dotr.com) or you can use the MAPI through its native API. On this site we'll only discuss the API kind of solutions.
The next sourcecode example was posted by Jeff Ledbetter and was earlier found in Profiles. I ported it to 32-bit.
The example uses the 'Simple MAPI' interface. This interface is not by default available on any machine. The easiest way to ensure if Simple MAPI is available is to check the registry: the "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Messaging Subsystem" variable "MAPI" should have value "1".
RUN SendMail ( INPUT "yourself@home.com", INPUT "this is the subject line", INPUT "This is the body text", OUTPUT Okay). PROCEDURE SendMail : DEFINE INPUT PARAMETER send-to-name AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER send-subject AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER send-text AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER Okay AS LOGICAL NO-UNDO INITIAL NO. DEFINE VARIABLE pnames AS MEMPTR. DEFINE VARIABLE psendto AS MEMPTR. DEFINE VARIABLE psubj AS MEMPTR. DEFINE VARIABLE ptext AS MEMPTR. DEFINE VARIABLE pmessage AS MEMPTR. DEFINE VARIABLE wans AS INTEGER . SET-SIZE(pnames) = 24. SET-SIZE(psendto) = 16. PUT-LONG(pnames,1) = 0. /* Reserved */ PUT-LONG(pnames,5) = 1. /* Recip Class MAPI_TO */ PUT-LONG(pnames,9) = GET-POINTER-VALUE(psendto). /* Names */ PUT-LONG(pnames,17) = 0. /* EID Size */ SET-SIZE(psubj) = 100. SET-SIZE(ptext) = 8000. SET-SIZE(pmessage) = 48. PUT-STRING(psubj,1) = send-subject. PUT-STRING(ptext,1) = send-text. PUT-STRING(psendto,1) = send-to-name. PUT-LONG(pmessage,1) = 0. /* Reserved */ PUT-LONG(pmessage,5) = GET-POINTER-VALUE(psubj). /* Subject */ PUT-LONG(pmessage,9) = GET-POINTER-VALUE(ptext). /* Text */ PUT-LONG(pmessage,25) = 0. /* Flags */ PUT-LONG(pmessage,33) = 1. /* RecipCount */ PUT-LONG(pmessage,37) = GET-POINTER-VALUE(pnames). PUT-LONG(pmessage,41) = 0. RUN MAPISendMail IN hpApi(INPUT 0, /* mapi session handle */ INPUT 0, /* parent window handle */ INPUT GET-POINTER-VALUE(pmessage), INPUT 0, /* flags */ INPUT 0, /* reserved, must be 0 */ OUTPUT Wans). /* error status */ IF Wans<>0 THEN MESSAGE "Mail not sent, error code=" Wans VIEW-AS ALERT-BOX. ELSE Okay = YES. /* dealloc memory */ SET-SIZE(pnames) = 0. SET-SIZE(psendto) = 0. SET-SIZE(psubj) = 0. SET-SIZE(ptext) = 0. SET-SIZE(pmessage) = 0. END PROCEDURE.
The first parameter is a Mapi-session handle. If 0, MAPI will try to find a shared session or create a temporary session for the duration of the call and it might have to show a login dialog. So if you need to send several mails it might be better to create a session up front and reuse its handle.
The second parameter is a parent window handle. If MapiSendMail has to show a dialog (for example a login dialog), it will be an application-modal dialog parented to this window handle.The value 0 is valid but will not block input in your Progress application.
The third parameter is a fairly complicated message structure. This example only creates a simple mail but MAPI also allows to add one or more attachements as shown in the source on the next page ([[mapiattachment]] wikilink).
If you do not specify a recipient (send-to-name) you must allow MAPI to ask for one. That is, you must use the MAPI_DIALOG (=8) flag in the 4th parameter of MapiSendMail.
By Johann van der Merwe.
PROCEDURE MapiReturnCode : INPUT PARAMETER ResultInt AS INTEGER. /* result from MAPISendMail */ DEFINE VARIABLE RESULT AS CHARACTER NO-UNDO. IF ResultInt <> 0 THEN DO: /* 0 = Success */ CASE ResultInt: WHEN 1 THEN RESULT = "User Abort". WHEN 2 THEN RESULT = "Failure". WHEN 3 THEN RESULT = "Login Failure". WHEN 4 THEN RESULT = "Disk Full". WHEN 5 THEN RESULT = "Insufficient Memory". WHEN 6 THEN RESULT = "Blk Too Small". WHEN 8 THEN RESULT = "Too Many Sessions". WHEN 9 THEN RESULT = "Too Many Files". WHEN 10 THEN RESULT = "Too Many Recipients". WHEN 11 THEN RESULT = "Attachment Not Found". WHEN 12 THEN RESULT = "Attachment Open Failure". WHEN 13 THEN RESULT = "Attachment Write Failure". WHEN 14 THEN RESULT = "Unknown Recipient". WHEN 15 THEN RESULT = "Bad Recipient type". WHEN 16 THEN RESULT = "No Messages". WHEN 17 THEN RESULT = "Invalid Message". WHEN 18 THEN RESULT = "Bodytext Too Large". WHEN 19 THEN RESULT = "Invalid Session". WHEN 20 THEN RESULT = "Type Not Supported". WHEN 21 THEN RESULT = "Ambiguous Recipient". WHEN 22 THEN RESULT = "Message in use". WHEN 23 THEN RESULT = "Network failure". WHEN 24 THEN RESULT = "Invalid edit fields". WHEN 25 THEN RESULT = "Invalid recipients". WHEN 26 THEN RESULT = "Feature not supported" OTHERWISE RESULT = "Unknown error". END CASE. DO ON ENDKEY UNDO, LEAVE: MESSAGE ResultInt RESULT VIEW-AS ALERT-BOX. END. END.
Since this is a chapter about email, it should be noted there is an excellent program named smtpmail.p available for free at [broken link removed]
This program is entirely written in regular Progress ABL without any WIN32 API call.
You can use Microsoft's CDO object library to easily send emails from Progress using ActiveX.
It seems to use the local SMTP server installed with IIS, so you can get started with very little code.
def var objMessage as com-handle. create "CDO.Message" objMessage. objMessage:Subject = "Example CDO Message from Progress". objMessage:From = "example@example.com". objMessage:To = "example@example.com". objMessage:TextBody = "This is some sample message text.". objMessage:Send().
More extensive examples of using CDO (with VBScript) can be found here:
http://www.paulsadowski.com/WSH/cdo.htm
http://www.lewisroberts.com/2006/06/09/sending-cdomessage-with-importanc...
There are many ways to send email, smtpmail.p from freeframework.org is known to be very good.
In the past there have been many quesions about Outlook. Here is one code snippet, seen on Peg in an email from John Lubenow (I hope we have permission to use this, if not just delete this topic and let me know please)
/** Send Outlook Mail **/ DEFINE INPUT PARAMETER mailto AS CHARACTER FORMAT "x(30)". DEFINE INPUT PARAMETER mailcc AS CHARACTER FORMAT "x(30)". DEFINE INPUT PARAMETER subject AS CHARACTER FORMAT "x(50)". DEFINE INPUT PARAMETER body AS CHARACTER FORMAT "x(255)". DEFINE VARIABLE chOutlook AS COM-HANDLE NO-UNDO. DEFINE VARIABLE chmail AS COM-HANDLE NO-UNDO. CREATE "outlook.application.9" choutlook NO-ERROR. ASSIGN chmail = chOutlook:createItem(0). /** Assign data **/ ASSIGN chmail:Subject = subject. ASSIGN chMail:Body = body. ASSIGN chMail:TO = mailto. ASSIGN chMail:CC = mailcc. /** example for one attachment: **/ chMail:Attachments:Add( [insert-file-name-here] ) NO-ERROR. chMail:Attachments(1):DisplayName = [insert-file-display-name-here]. /** view mail **/ chMail:Display(1). chMail:SEND(). RELEASE OBJECT chmail. RELEASE OBJECT choutlook.
.
It is recommended to use the CreateProcess procedure instead WinExec. This is a very low-level procedure, it gives complete control over processes, threads and more.
CreateProcess takes 10 parameters, two of which are pointers to structures containing further parameters. Most parameters are not very interesting for our needs, so I have made a function CreateProcess that only takes the three most important params. The function is described in winfunc.p and can be called like in this example:
{windows.i} DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. hProcess = CreateProcess("notepad.exe c:\config.sys", "", 1). IF hProcess=0 THEN ShowLastError(). RUN CloseHandle IN hpApi (hProcess, OUTPUT ReturnValue).
The first parameter is the command line with optional parameters. The second parameter is the working directory for the new process, where "" will result in the current working directory of the Progress application. The third parameter is cmdShow with the same meaning as in WinExec.
The return value is a handle to the new process or 0 if the call failed.
The GetLastError procedure did not work well in Progress 8, but it is fine now.
Do not forget to call CloseHandle. MS-Windows keeps a reference count to each kernel object, a process is a kernel object. A kernel object can not be destroyed when its reference count is larger than zero. Run CloseHandle decrements the reference count by one which only tells the kernel that you are not interested in keeping the new process alive. The process itself also has at least one reference to itself, so this CloseHandle call will not actually terminate the process. Forgetting to call CloseHandle will result in resource loss.
The following procedure also uses CreateProcess, but this time it returns a PID (process identifier) instead of a process handle.
This can be useful because procedure KillProcess expects a PID as input parameter, see terminate a process gently.
{windows.i} PROCEDURE MakeProcess : DEFINE INPUT PARAMETER CommandLine AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER WorkingDir AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER PID AS INTEGER NO-UNDO. DEFINE VARIABLE wShowWindow AS INTEGER NO-UNDO INITIAL 0. DEFINE VARIABLE bResult AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE lpStartupInfo AS MEMPTR. SET-SIZE(lpStartupInfo) = 68. PUT-LONG(lpStartupInfo,1) = 68. PUT-LONG (lpStartupInfo,45) = 1. /* = STARTF_USESHOWWINDOW */ PUT-SHORT(lpStartupInfo,49) = wShowWindow. DEFINE VARIABLE lpProcessInformation AS MEMPTR. SET-SIZE(lpProcessInformation) = 16. DEFINE VARIABLE lpWorkingDirectory AS MEMPTR. IF WorkingDir NE "" THEN DO: SET-SIZE(lpWorkingDirectory) = 256. PUT-STRING(lpWorkingDirectory,1) = WorkingDir. END. RUN CreateProcessA IN hpApi ( 0, CommandLine, 0, 0, 0, 0, 0, IF WorkingDir="" THEN 0 ELSE GET-POINTER-VALUE(lpWorkingDirectory), GET-POINTER-VALUE(lpStartupInfo), GET-POINTER-VALUE(lpProcessInformation), OUTPUT bResult ). IF bResult=0 THEN PID = 0. ELSE DO: PID = GET-LONG(lpProcessInformation,9). /* release kernel-objects hProcess and hThread: */ RUN CloseHandle IN hpApi(GET-LONG(lpProcessInformation,1), OUTPUT ReturnValue). RUN CloseHandle IN hpApi(GET-LONG(lpProcessInformation,5), OUTPUT ReturnValue). END. SET-SIZE(lpStartupInfo) = 0. SET-SIZE(lpProcessInformation) = 0. SET-SIZE(lpWorkingDirectory) = 0. END PROCEDURE.
Suppose your window has a button that is designed to launch an application, using ShellExecute or CreateProcess, but this application takes a few seconds to launch. Since ShellExecute(Ex) and CreateProcess return immediately after the process is created, an impatient user will have time to press the button several times. To work around this problem you may want to disable the button until the application really becomes visible. WaitForInputIdle does the trick. Well, in reality it waits until the application has its input queue (for mouse and keyboard) empty and waiting, but that's at about the same time anyway.
{windows.i} DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. hProcess = CreateProcess("notepad.exe", "", 3). IF hProcess = 0 THEN DO: getlasterror(). ShowLastError(). END. ELSE DO: RUN WaitForInputIdle IN hpApi (hProcess, -1, /* -1=INFINITE */ OUTPUT ReturnValue). RUN CloseHandle IN hpApi (hProcess, OUTPUT ReturnValue). END.
CreateProcess does not search for applications as good as ShellExecute does. When CreateProcess does not find an application you may have more luck with ShellExecuteEx (not ShellExecute itself, because that does not return a hProcess).
ShellExecuteEx will return a hProcess when SEE_MASK_NOCLOSEPROCESS is specified in its fMask field.
This code was found on PEG, posted by Stuart Butler a while ago. It is 16 bit code and needs to be reworked for 32-bit. It compiles in version 8.2 but does not wait as expected.
The example program runs notepad, waits until the user has closed notepad and then displays the results of the user's work with notepad.
/* If we were being run persistent we would want to *** be interrupted when waiting for notepad to finish *** if we are closed so include an on close event trigger */ DEFINE VARIABLE lgClosing AS LOGICAL INITIAL FALSE. ON CLOSE OF THIS-PROCEDURE DO: lgClosing = TRUE. END. /* Run Notepad and keep a record of its task number */ DEFINE VARIABLE nTask AS INT. RUN WinExec IN hpApi( "Notepad c:\temp\trash.txt", 1, OUTPUT nTask ). IF nTask GT 0 AND nTask LT 32 THEN DO: MESSAGE "WinExec failed" VIEW-AS ALERT-BOX. RETURN. END. /* Now wait for notepad to finish - when notepad is closed *** the task number will become invalid and Module *** API functions will fail - we are using GetModuleFileName. *** The WAIT-FOR will process events and not hog all the CPU *** - pity the PAUSE could not be in milliseconds but will have to do *** with what's available. If you use PROCESS EVENTS in some *** other loop construction (with say a timer VBX) it is a good idea to *** preceed it with a call to the WaitMessage() API call as *** looping with PROCESS EVENTS tends to hog the CPU. *** You might want to disable your user interface at this point *** though that may not be necessary if you have run this .p *** persistently to deal with the windows app "in the background". */ DEFINE VARIABLE szName AS CHARACTER. DEFINE VARIABLE szNameLength AS INTEGER. REPEAT: WAIT-FOR CLOSE OF THIS-PROCEDURE PAUSE 1. szName = FILL(" ",256). RUN GetModuleFileName{&A} IN hpApi( nTask, OUTPUT szName, LENGTH(szName), OUTPUT szNameLength). /* We don't want to know the ModuleFileName, we only want *** to know if the function fails. Fails if result=0. *** A failure indicates that the task has ended. */ IF (szNameLength=0) OR lgClosing THEN LEAVE. END. /* Notepad should have finished at this point - let's *** see what the user input */ MESSAGE "Notepad finished" VIEW-AS ALERT-BOX. INPUT FROM c:\temp\trash.txt. DEFINE VARIABLE szLine AS CHARACTER. REPEAT: IMPORT UNFORMATTED szLine. MESSAGE szLine VIEW-AS ALERT-BOX. END. INPUT CLOSE.
Unfortunately, the repeat loop that checks for the existence of the executing task may slow down the computer.
In a 32-bit environment you can use the procedure WaitForSingleObject. This is a very efficient procedure. The input parameter for WaitForSingleObject is a handle to an object, in this case a kernel process object. This is not the same as the hInstance as returned by ShellExecute.
To obtain a process handle you seem to have to start the process using either CreateProcess or ShellExecuteEx.
Here is a (simplified) example for CreateProcess. The next page, titled "wait until MS-Word finished printing" uses ShellExecuteEx but not specifically for its process handle...
{windows.i} DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. hProcess = CreateProcess("notepad.exe c:\config.sys", "", 1). IF hProcess=0 THEN ShowLastError(). ELSE DO: RUN WaitForSingleObject IN hpApi (hProcess, -1, /* -1=INFINITE */ OUTPUT ReturnValue). RUN CloseHandle IN hpApi (hProcess, OUTPUT ReturnValue). END
CreateProcess and ShowLastError are declared in windows.i.
The process object has an internal reference counter. There are/were at least two references: one from within notepad.exe itself, the other one from your program (obtained by CreateProcess). When notepad.exe terminates, it will decrease the object's reference counter but your program still holds one. The object can not be destroyed until the reference counter decreases to zero, so you must use CloseHandle (which decrements the counter by one and invalidates the handle).
In other words: there is a memory leak if you don't call CloseHandle (although the kernel calls it when the Progress session terminates).
CloseHandle does not destroy the object (yet), it just tells the Kernel that it can destroy the object whenever it wants to (as far as you are concerned). CloseHandle is important for every Kernel object: CreateProcess returns a process object and also a thread object. The thread object is already closed inside the P4GL implementation of function CreateProcess in winfunc.p (version May 9, 1998).
ShellExecute is the procedure that is called by the Desktop or Windows Explorer when you double-click an item: if the item is an executable it will run it, if the item is a Word-document it will open it in MS-Word, etc.
There is a lot you can do with this easy-to-use procedure. Let's have a look at the parameters:
PROCEDURE ShellExecute{&A} EXTERNAL "shell32" : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER lpOperation AS CHARACTER. DEFINE INPUT PARAMETER lpFile AS CHARACTER. DEFINE INPUT PARAMETER lpParameters AS CHARACTER. DEFINE INPUT PARAMETER lpDirectory AS CHARACTER. DEFINE INPUT PARAMETER nShowCmd AS LONG. DEFINE RETURN PARAMETER hInstance AS LONG. END.
The parameters are:
* hwnd : parent window that will receive a possible messagebox. This parameter is usually 0
* lpOperation : "open" or "print"
"print" is only valid if lpFile is a document
* lpFile : name of an executable or name of a document
* lpParameters : command-line parameters for the executable in lpFile
* lpDirectory : default directory to execute lpFile in
* nShowCmd : Same as in winexec: hidden=0 normal=1 minimized=2 maximized=3
* hInstance : If function succeeds hInstance will be the instance handle of the executed program. If the value is >=0 and <=32 the function failed.
As you can see there are different ways to perform "notepad.exe c:\readme.txt" :
RUN ShellExecute{&A} IN hpApi(0, "open", "notepad.exe", "c:\readme.txt", "", 1, OUTPUT hInstance).
RUN ShellExecute{&A} IN hpApi(0, "open", "c:\readme.txt", "", "", 1, OUTPUT hInstance).
The second example shows an important feature of ShellExecute: opening a document. It uses associations to find the matching executable. Both examples will have the exact same result if the extention ".txt" is associated with "notepad.exe".
The following examples, submitted by Rob den Boer, show how ShellExecute can be used to send email, or to open the default browser for a particular website:
DEFINE VARIABLE email AS CHARACTER NO-UNDO. email = "mailto:rc.den.boer@hccnet.nl" + "?cc=piet@nowhere.nl" + "?Subject=Test%20by%20Rob" /* notice %20 instead of space */ + "?Body=How are you doing". RUN ShellExecute{&A} IN hpApi (0, "open", email, "", "", 1, OUTPUT hInstance).
RUN ShellExecute{&A} IN hpApi (0, "open", "ftp://zdftp.zdnet.com", "", "", 1, OUTPUT hInstance). /* or: */ RUN ShellExecute{&A} IN hpApi (0, "open", "http://home.hccnet.nl/rc.den.boer/progress/index.html", "", "", 1, OUTPUT hInstance).
improved by Tim Townsend
Documents can be associated with executables, Windows recognizes a document by its file extention.
Thanks to associations, the ShellExecute procedure knows how to "open" a document and the Explorer knows which icon to draw next to a document.
If a document does not have an association yet and you choose "open" in the Explorer, you will be presented a "Open As" dialog where you can choose an application. The following example shows how to do this in Progress.
The procedure first tries to open (or print) the document. If this fails the first time, it will try a second time using the OpenAs dialog.
/*-------------------------------------------------------------------------- File : open-doc.p Purpose : Open a windows document using the associated application. If no assocoated application, run the OpenAs dialog to allow the user to pick an application. Syntax : Description : Author(s) : TWT Created : 22 Dec 1999 Notes : --------------------------------------------------------------------------*/ DEFINE INPUT PARAM cFileName AS CHARACTER NO-UNDO. DEFINE INPUT PARAM cParams AS CHARACTER NO-UNDO. DEFINE INPUT PARAM cDirectory AS CHARACTER NO-UNDO. DEFINE INPUT PARAM lPrint AS LOG NO-UNDO. &SCOPED-DEFINE SE_ERR_NOASSOC 31 &SCOPED-DEFINE SE_ERR_ASSOCINCOMPLETE 27 DEFINE VARIABLE iInstance AS INTEGER NO-UNDO. DEFINE VARIABLE cWorkDirectory AS CHARACTER NO-UNDO. /* in case parameter cDirectory contains a relative path it has to be replaced by a fully-qualified path: */ ASSIGN FILE-INFO:FILE-NAME = cDirectory. IF FILE-INFO:FULL-PATHNAME > "" THEN cWorkDirectory = FILE-INFO:FULL-PATHNAME. /* try to execute the document: */ RUN ShellExecuteA(INPUT 0, INPUT (IF lPrint THEN "print":u ELSE "open":u), INPUT cFileName, INPUT cParams, INPUT cWorkDirectory, INPUT 1, /* normal mode */ OUTPUT iInstance). /* if no associated application, run OpenAs dialog: */ IF (iInstance = {&SE_ERR_NOASSOC} OR iInstance = {&SE_ERR_ASSOCINCOMPLETE}) AND NOT lPrint THEN DO: /* Ignore cParams because cFileName is a document. cParams is only valid with executables */ RUN ShellExecuteA (INPUT 0, INPUT "open":u, INPUT "rundll32.exe":u, INPUT "shell32.dll,OpenAs_RunDLL ":u + cFileName, INPUT cWorkDirectory, INPUT 1, OUTPUT iInstance). END. /* if */ /* test for error: */ RUN TestErrorCode(iInstance). IF RETURN-VALUE > "" THEN MESSAGE RETURN-VALUE VIEW-AS ALERT-BOX ERROR BUTTON OK. /****************************************************************************/ PROCEDURE ShellExecuteA EXTERNAL "shell32":U : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER lpOperation AS CHARACTER. DEFINE INPUT PARAMETER lpFile AS CHARACTER. DEFINE INPUT PARAMETER lpParameters AS CHARACTER. DEFINE INPUT PARAMETER lpDirectory AS CHARACTER. DEFINE INPUT PARAMETER nShowCmd AS LONG. DEFINE RETURN PARAMETER hInstance AS LONG. END PROCEDURE. PROCEDURE TestErrorCode : DEFINE INPUT PARAMETER iCode AS INTEGER. DEFINE VARIABLE cTxt AS CHARACTER NO-UNDO. IF iCode < 0 OR iCode > 32 THEN RETURN "". /* no error */ CASE iCode : WHEN 0 THEN cTxt = "The operating system is out of memory or resources.":T132. WHEN 2 THEN cTxt = "The specified file was not found":T132. WHEN 3 THEN cTxt = "The specified path was not found.":T132. WHEN 5 THEN cTxt = "The operating system denied access to the specified file.":T132. WHEN 8 THEN cTxt = "There was not enough memory to complete the operation.":T132. WHEN 10 THEN cTxt = "Wrong Windows version":T132. WHEN 11 THEN cTxt = "The .EXE file is invalid (non-Win32 .EXE or error in .EXE image).":T132. WHEN 12 THEN cTxt = "Application was designed for a different operating system.":T132. WHEN 13 THEN cTxt = "Application was designed for MS-DOS 4.0.":T132. WHEN 15 THEN cTxt = "Attempt to load a real-mode program.":T132. WHEN 16 THEN cTxt = "Attempt to load a second instance of an application with non-readonly data segments.":T132. WHEN 19 THEN cTxt = "Attempt to load a compressed application file.":T132. WHEN 20 THEN cTxt = "Dynamic-link library (DLL) file failure.":T132. WHEN 26 THEN cTxt = "A sharing violation occurred.":T132. WHEN 27 THEN cTxt = "The filename association is incomplete or invalid.":T132. WHEN 28 THEN cTxt = "The DDE transaction could not be completed because the request timed out.":T132. WHEN 29 THEN cTxt = "The DDE transaction failed.":T132. WHEN 30 THEN cTxt = "The DDE transaction could not be completed because other DDE transactions were being processed.":T132. WHEN 31 THEN cTxt = "There is no application associated with the given filename extension.":T132. WHEN 32 THEN cTxt = "The specified dynamic-link library was not found.":T132. OTHERWISE cTxt = "Undocumented error code returned":T132. END. RETURN cTxt. END PROCEDURE.
This example by Tim Townsend replaces the old example, which was published here until 5 januari 2000. For those who still use a copy of the old source: this new one is better because you can now pass a URL, like "www.progress.com", to the cFilename parameter.
If a call to ShellExecute succeeds, the returned hInstance parameter will be the instance handle of the executed program. But if the hInstance is less then 33, it indicates a failure.
Actually if the hInstance>=0 and hInstance<33 because a very large hInstance will be casted into a signed integer variable and may appear to be negative from Progress' perspective.
This page lists the descriptions for some of the possible failures.
It's actually a combination from different resources, both from WinExec and other procedures so some of the result may never occur when using ShellExecute. There are also some gaps in the list, and moreover: some results should have a different description in 16-bit mode. Where descriptions in 16-bit are different then 32-bit I've picked the 32-bit description
{windows.i} DEFINE VARIABLE hInstance AS INTEGER. RUN ShellExecute{&A} IN hpApi(0, "open", "xyz.doc", "", "", 1, OUTPUT hInstance). RUN TestInstance (hInstance). IF RETURN-VALUE > "" THEN MESSAGE "ShellExecute failed, reason: " SKIP RETURN-VALUE VIEW-AS ALERT-BOX ERROR. PROCEDURE TestInstance : DEFINE INPUT PARAMETER hInstance AS INTEGER. /* =return value from ShellExecute */ DEFINE VARIABLE txt AS CHARACTER NO-UNDO. IF hInstance<0 OR hInstance>32 THEN RETURN "". /* not failed */ CASE hInstance : WHEN 0 THEN txt = "The operating system is out of memory or resources.". WHEN 2 THEN txt = "The specified file was not found". WHEN 3 THEN txt = "The specified path was not found.". WHEN 5 THEN txt = "Windows 95 only: The operating system denied " + "access to the specified file". WHEN 8 THEN txt = "Windows 95 only: There was not enough memory to " + "complete the operation.". WHEN 10 THEN txt = "Wrong Windows version". WHEN 11 THEN txt = "The .EXE file is invalid (non-Win32 .EXE or " + "error in .EXE image).". WHEN 12 THEN txt = "Application was designed for a different operating system". WHEN 13 THEN txt = "Application was designed for MS-DOS 4.0". WHEN 15 THEN txt = "Attempt to load a real-mode program". WHEN 16 THEN txt = "Attempt to load a second instance of " + "an application with non-readonly data segments". WHEN 19 THEN txt = "Attempt to load a compressed application file". WHEN 20 THEN txt = "Dynamic-link library (DLL) file failure". WHEN 26 THEN txt = "A sharing violation occurred.". WHEN 27 THEN txt = "The filename association is incomplete or invalid.". WHEN 28 THEN txt = "The DDE transaction could not be completed " + "because the request timed out.". WHEN 29 THEN txt = "The DDE transaction failed.". WHEN 30 THEN txt = "The DDE transaction could not be completed because " + "other DDE transactions were being processed.". WHEN 31 THEN txt = "There is no application associated with " + "the given filename extension.". WHEN 32 THEN txt = "Windows 95 only: The specified dynamic-link " + "library was not found.". OTHERWISE txt = "undocumented". END. RETURN txt. END PROCEDURE.
ShellExecuteEx combines features of ShellExecute and CreateProcess, the most noticable feature is that it returns a PID.
PROCEDURE ShellExecuteExA EXTERNAL "shell32.dll" : DEFINE INPUT PARAMETER lpExecInfo AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.
lpExecInfo is a memory-pointer to a structure of type SHELLEXECUTEINFO, which is a bit complicated to describe. Perhaps it's best to point at an example: See wait until MS-Word finished printing.
WinExec is often found in old examples for Visual Basic. It's old... don't use it!
In Windows 3.x you would have used the WinExec procedure to run a program. The procedure is still around in Windows but it is obsolete, you should use CreateProcess or ShellExecute instead. In fact, WinExec itself is nothing but a wrapper to CreateProcess.
Note that WinExec from "krnl386.exe" (the 16 bit version) can not execute a 32-bit program, while WinExec from "kernel32.dll" (the 32 bit version) can't execute a 16-bit program.
So if you are running a Progress version less than 8.2 on Windows 95+, you may end up using two different versions of WinExec (the regular 16-bits version and a thunked 32-bits version).
(Also if you want to call a 16-bit program in Progress 8.2, but that is something you should not want to do!)
The WinExec procedure does not let you control the default directory.
If all these problems don't apply to you, you will find WinExec easy to use. Here is how you call it:
{windows.i} DEFINE VARIABLE hTask AS INTEGER NO-UNDO. RUN WinExec IN hpApi( "notepad.exe c:\readme.txt", 1, OUTPUT hTask). IF hTask>=0 AND hTask<32 THEN MESSAGE "failed" VIEW-AS ALERT-BOX.
The second parameter is the CmdShow parameter, the most commonly used values are:
* hidden = 0
* normal = 1
* minimized = 2
* maximized = 3
The output parameter is a handle to the task that WinExec created. According to API documentation this value should be greater than 31. But since it is an unsigned integer and gets casted into a signed Progress integer, it may occur that a valid very large value will seem to be less than 0. Values 0 to 31 represent a documented error status.
* You have created an RTF file, c:\temp\test.rtf
* You want MS-Word to print this RTF file, but you don't want to use OLE Automation. In other words: you want to call ShellExecute(..., "print", "c:\temp\test.rtf", .....).
* You want to wait until MS-Word has finished printing.
OLE Automation would be fine but the OLE commands are application specific (and perhaps even version/translation specific) so you rather rely on the Shell "print"-verb.
You can not easily use CreateProcess and WaitForSingleObject, because MS-Word (and many other word-processors) has an MDI-interface. This means MS-Word (the process) will not terminate if it was already opened before you issued the "print"-request.
Knowing that the Shell's "print"-verb executes a DDE-conversation with MS-Word, you can use ShellExecuteEx with the option to wait until the DDE conversation terminates.
Procedure PrintAndWait in this example uses the ShellExecuteEx function. I did not include any error-handling.
RUN PrintAndWait ("c:\temp\test.rtf"). {windows.i} PROCEDURE PrintAndWait : define INPUT PARAMETER FILENAME AS CHARACTER NO-UNDO. DEFINE VARIABLE lpVerb AS MEMPTR. DEFINE VARIABLE lpFile AS MEMPTR. DEFINE VARIABLE lpExecInfo AS MEMPTR. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpVerb) = LENGTH("print") + 1. PUT-STRING(lpVerb,1) = "print". SET-SIZE(lpFile) = LENGTH (FILENAME) + 1. PUT-STRING(lpFile,1) = FILENAME. SET-SIZE (lpExecInfo) = 60. PUT-LONG (lpExecInfo, 1) = GET-SIZE(lpExecInfo). PUT-LONG (lpExecInfo, 5) = 256. /* = SEE_MASK_FLAG_DDEWAIT */ PUT-LONG (lpExecInfo, 9) = 0. /* hwnd */ PUT-LONG (lpExecInfo,13) = GET-POINTER-VALUE(lpVerb). PUT-LONG (lpExecInfo,17) = GET-POINTER-VALUE(lpFile). PUT-LONG (lpExecInfo,21) = 0. /* commandline */ PUT-LONG (lpExecInfo,25) = 0. /* current directory */ PUT-LONG (lpExecInfo,29) = 2. /* wCmdShow */ RUN ShellExecuteExA IN hpApi(GET-POINTER-VALUE(lpExecInfo), OUTPUT ReturnValue). SET-SIZE (lpExecInfo) = 0. SET-SIZE (lpFile) = 0. SET-SIZE (lpverb) = 0. END PROCEDURE.
.
by Michael Rüsweg-Gilbert
To get the amount of available disk space you can call GetDiskFreeSpace or GetDiskFreeSpaceEx.
GetDiskFreeSpaceEx is best because it supports disks larger than 2 gigabyte. Unfortunately this function may not be available on every Windows version (Windows 95 before OSR2 only supports GetDiskFreeSpace).
The following program shows how to call GetDiskFreeSpaceEx. Note how the 64-bit parameters are cast in MEMPTR variables and converted to decimal values.
/* =============================================================================== Program: VolSpace.p Created: Michael R?Gilbert Feb 2001 mailto:rg@rgilbert.de Description: returns the capacity and free space of a volume (even if Vol > 2 GB) Usage: for ex. run VolSpace.p ("C:", "KB", output freeSpace, output totalSpace). Parameters: - Volume to check or Blank (Blank returns informations abaout the working drirector drive) It does not have to be the root, accepts any directory. - Unit to format the result; legal entries are "KB", "MB" or "GB". If the unit is not recognized or empty, VolSpace will return Number of Bytes. - OUTPUT available free space in given unit - OUTPUT total space in given unit When VolSpace is not successful, both output parameters will return ?. modifications: March 14, 2001: Jurjen - added function IsAPIFunctionSupported() ===================================================================================== */ DEFINE INPUT PARAMETER ip_drive AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER ip_unit AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER op_free AS DECIMAL NO-UNDO. DEFINE OUTPUT PARAMETER op_total AS DECIMAL NO-UNDO. &SCOPED-DEFINE WTRUE 1 &SCOPED-DEFINE WFALSE 0 FUNCTION get64BitValue RETURNS DECIMAL (INPUT m64 AS MEMPTR) FORWARD. FUNCTION IsAPIFunctionSupported RETURNS LOGICAL (FunctionName AS CHAR, ModuleName AS CHARACTER) FORWARD. DEFINE VARIABLE retval AS INTEGER NO-UNDO. DEFINE VARIABLE divident AS INTEGER NO-UNDO INIT 1. DEFINE VARIABLE i AS INTEGER NO-UNDO. DEFINE VARIABLE mem1 AS MEMPTR NO-UNDO. DEFINE VARIABLE mem2 AS MEMPTR NO-UNDO. DEFINE VARIABLE mem3 AS MEMPTR NO-UNDO. /* See if GetDiskFreeSpaceEx is available in this Windows version. (it is available in NT4, Windows 95 OSR2, Windows 98, Windows 2000) */ IF NOT IsAPIFunctionSupported("GetDiskFreeSpaceExA":U, "kernel32.dll":U) THEN DO: MESSAGE "Sorry, your version of Windows does not support GetDiskFreeSpaceEx" VIEW-AS ALERT-BOX. ASSIGN op_free = ? op_total = ?. RETURN. END. IF CAN-DO("KB,Kilo,Kilobyte,Kilobytes", ip_unit) THEN divident = 1024. ELSE IF CAN-DO("MB,Mega,Megabyte,Megabytes", ip_unit) THEN divident = 1024 * 1024. ELSE IF CAN-DO("GB,Giga,Gigabyte,Gigabytes", ip_unit) THEN divident = 1024 * 1024 * 1024. ELSE divident = 1. /* No directory specified? Then use the current directory */ IF (ip_drive = "") OR (ip_drive=?) THEN DO: FILE-INFO:FILE-NAME = ".". ip_drive = FILE-INFO:FULL-PATHNAME. END. /* If a UNC name was specified, make sure it ends with a backslash ( \\drive\share\dir\ ) This won't hurt for a mapped drive too */ IF SUBSTR(ip_drive, LENGTH(ip_drive), 1) NE "\" THEN ip_drive = ip_drive + "\". SET-SIZE(mem1) = 8. /* 64 bit integer! */ SET-SIZE(mem2) = 8. SET-SIZE(mem3) = 8. RUN GetDiskFreeSpaceExA ( ip_drive + CHR(0), OUTPUT mem1, OUTPUT mem2, OUTPUT mem3, OUTPUT retVal ). IF retVal NE {&WTRUE} THEN DO: op_free = ?. op_total = ?. END. ELSE DO: ASSIGN op_free = TRUNC( get64BitValue(mem3) / divident, 3) op_total = TRUNC( get64BitValue(mem2) / divident, 3). END. SET-SIZE(mem1) = 0. SET-SIZE(mem2) = 0. SET-SIZE(mem3) = 0. RETURN. PROCEDURE GetModuleHandleA EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER lpModuleName AS CHARACTER NO-UNDO. DEFINE RETURN PARAMETER hModule AS LONG NO-UNDO. END PROCEDURE. PROCEDURE GetProcAddress EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hModule AS LONG NO-UNDO. DEFINE INPUT PARAMETER lpProcName AS CHARACTER NO-UNDO. DEFINE RETURN PARAMETER lpFarproc AS LONG NO-UNDO. END PROCEDURE. PROCEDURE GetDiskFreeSpaceExA EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER lpDirectoryName AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER FreeBytesAvailable AS MEMPTR NO-UNDO. DEFINE OUTPUT PARAMETER TotalNumberOfBytes AS MEMPTR NO-UNDO. DEFINE OUTPUT PARAMETER TotalNumberOfFreeBytes AS MEMPTR NO-UNDO. DEFINE RETURN PARAMETER retval AS LONG NO-UNDO. END PROCEDURE. /* See if GetDiskFreeSpaceEx is available in this Windows version */ FUNCTION IsAPIFunctionSupported RETURNS LOGICAL (FunctionName AS CHAR, ModuleName AS CHARACTER): DEFINE VARIABLE hModule AS INTEGER NO-UNDO. DEFINE VARIABLE lpFarProc AS INTEGER NO-UNDO. /* you should run LoadLibraryA to load the module into memory, but this is not necessary for ModuleName="kernel32.dll": the kernel is always available. */ RUN GetModuleHandleA (ModuleName, OUTPUT hModule). RUN GetProcAddress (hModule, FunctionName, OUTPUT lpFarProc). RETURN lpFarProc NE 0. END FUNCTION. /* Converts a 64-bit integer given in a 8 byte mempointer into a decimal */ FUNCTION get64BitValue RETURNS DECIMAL ( INPUT m64 AS MEMPTR ): /* constant 2^32 */ &SCOPED-DEFINE BigInt 4294967296 DEFINE VARIABLE d1 AS DECIMAL NO-UNDO. DEFINE VARIABLE d2 AS DECIMAL NO-UNDO. d1 = GET-LONG(m64, 1). IF d1 < 0 THEN d1 = d1 + {&BigInt}. d2 = GET-LONG(m64, 5). IF d2 < 0 THEN d2 = d2 + {&BigInt}. IF d2 GT 0 THEN d1 = d1 + (d2 * {&BigInt}). RETURN d1. END FUNCTION.
To get the amount of available disk space you can call GetDiskFreeSpace or GetDiskFreeSpaceEx.
There are differences between Windows 95, Windows 95 OSR/2 and Windows NT 4.0.
This text is quoted from MSDN Library:
In Windows 95, the existing Win32 function GetDiskFreeSpace may obtain incorrect values for volumes that are larger than 2 gigabytes (GB). In OSR 2, the function GetDiskFreeSpace has been modified to cap the value returned and never reports volume sizes greater than 2 GB. On very large empty volumes, existing applications will see only 2 GB free. If less than 2 GB are free, the correct amount will be returned. Windows 95 OSR 2 and Windows NT 4.0 support the GetDiskFreeSpaceEx function. GetDiskFreeSpaceEx obtains correct values on all platforms for all volumes, including those that are larger than 2 GB. New applications should use the GetDiskFreeSpaceEx function instead of the GetDiskFreeSpace function.
The following code shows how to call GetDiskFreeSpace. This example was submitted by Stuart Morris [stuart@IBS-PUBLIC-SERVICES.CO.UK]
DEFINE VARIABLE iSectorsPerCluster AS INTEGER NO-UNDO. DEFINE VARIABLE iBytesPerSector AS INTEGER NO-UNDO. DEFINE VARIABLE iFreeClusters AS INTEGER NO-UNDO. DEFINE VARIABLE iClusters AS INTEGER NO-UNDO. DEFINE VARIABLE iResult AS INTEGER NO-UNDO. DEFINE VARIABLE iVolName AS CHARACTER NO-UNDO INIT "c:\". PROCEDURE GetDiskFreeSpaceA EXTERNAL "kernel32.dll": DEFINE INPUT PARAM lpRootPathName AS CHARACTER. DEFINE OUTPUT PARAM opSectorsPerCluster AS LONG. DEFINE OUTPUT PARAM opBytesPerSector AS LONG. DEFINE OUTPUT PARAM opFreeClusters AS LONG. DEFINE OUTPUT PARAM opClusters AS LONG. DEFINE RETURN PARAM bResult AS LONG. END PROCEDURE. RUN GetDiskFreeSpaceA(INPUT iVolName, OUTPUT iSectorsPerCluster, OUTPUT iBytesPerSector, OUTPUT iFreeClusters, OUTPUT iClusters, OUTPUT iResult ). MESSAGE "SectorsPerCluster = " iSectorsPerCluster SKIP "BytesPerSector = " iBytesPerSector SKIP "FreeClusters = " iFreeClusters SKIP "Clusters = " iClusters SKIP(1) "BytesPerCluster = " (iSectorsPerCluster * iBytesPerSector) SKIP "DiskSpaceFree = " ((iFreeClusters * iSectorsPerCluster) * iBytesPerSector) VIEW-AS ALERT-BOX INFO TITLE "disk " + iVolname.
Sometimes you want to choose a folder but the usual system dialogs require you to choose a file as well. The API function SHBrowseForFolder does not show any files: it does exactly what the name implies.
/* a test/demo program */ DEFINE VARIABLE folder AS CHARACTER NO-UNDO. DEFINE VARIABLE canceled AS LOGICAL NO-UNDO. RUN BrowseForFolder.p ("choose the directory where you want to dump your data", OUTPUT folder, OUTPUT canceled). MESSAGE "folder=" folder SKIP "canceled=" canceled VIEW-AS ALERT-BOX. /* ========================================================== file: BrowseForFolder.p ========================================================== */ {windows.i} DEFINE INPUT PARAMETER DialogTitle AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER FolderName AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER Canceled AS LOGICAL NO-UNDO. DEFINE VARIABLE MAX_PATH AS INTEGER INITIAL 260. DEFINE VARIABLE lpbi AS MEMPTR. /* pointer to BROWSEINFO structure */ DEFINE VARIABLE pszDisplayName AS MEMPTR. DEFINE VARIABLE lpszTitle AS MEMPTR. DEFINE VARIABLE lpItemIDList AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpbi) = 32. SET-SIZE(pszDisplayName) = MAX_PATH. SET-SIZE(lpszTitle) = LENGTH(DialogTitle) + 1. PUT-STRING(lpszTitle,1) = DialogTitle. PUT-LONG(lpbi, 1) = 0. /* hwnd for parent */ PUT-LONG(lpbi, 5) = 0. PUT-LONG(lpbi, 9) = GET-POINTER-VALUE(pszDisplayName). PUT-LONG(lpbi,13) = GET-POINTER-VALUE(lpszTitle). PUT-LONG(lpbi,17) = 1. /* BIF_RETURNONLYFSDIRS = only accept a file system directory */ PUT-LONG(lpbi,21) = 0. /* lpfn, callback function */ PUT-LONG(lpbi,25) = 0. /* lParam for lpfn */ PUT-LONG(lpbi,29) = 0. RUN SHBrowseForFolder IN hpApi ( INPUT GET-POINTER-VALUE(lpbi), OUTPUT lpItemIDList ). /* parse the result: */ IF lpItemIDList=0 THEN DO: Canceled = YES. FolderName = "". END. ELSE DO: Canceled = NO. FolderName = FILL(" ", MAX_PATH). RUN SHGetPathFromIDList IN hpApi(lpItemIDList, OUTPUT FolderName, OUTPUT ReturnValue). FolderName = TRIM(FolderName). END. /* free memory: */ SET-SIZE(lpbi)=0. SET-SIZE(pszDisplayName)=0. SET-SIZE(lpszTitle)=0. RUN CoTaskMemFree (lpItemIDList). PROCEDURE CoTaskMemFree EXTERNAL "ole32.dll" : DEFINE INPUT PARAMETER lpVoid AS LONG. END PROCEDURE.
Documentation says that SHBrowseForFolder is not supported on Windows NT. However the above procedure was tested on Windows NT and seemed to work fine.
The memory occupied by lpItemIDList can be freed by CoTaskMemFree. This was discovered by Todd G. Nist who explains "This will free the memory the shell allocated for the ITEMIDLIST structure which consists of one or more consecutive ITEMIDLIST structures packed on byte boundaries, followed by a 16-bit zero value. An application can walk a list of item identifiers by examining the size specified in each SHITEMID structure and stopping when it finds a size of zero. A pointer to an item identifier list, is called a PIDL (pronounced piddle.) "
An different but very interesting approach is to use COM Automation: the "shell.application" interface contains a BrowseForFolder function. There is an example in article 18823 of the Progress Knowledgebase.
There is a different example by Julian Lyndon-Smith on page BrowseForFolder using COM
SHBrowseForFolder supports the use of a callback function from where you can specify an initial folder or perform some validations. Unfortunately, callback functions can not be written in Progress 4GL so you will have to wrap it in a DLL. This has been done by Cyril O'Floinn, see BrowseForFolder with an initial folder
Instead of using API-function SHBrowseForFolder to select a directory, you can also use the COM-interface of the "Shell".
There is an example in article 18823 of the Progress Knowledgebase.
Julian Lyndon-Smith wrote the following example, different from the one in the Knowledge Base.
FUNCTION DotRGetFolder RETURNS CHARACTER ( INPUT ip_cTitle AS CHARACTER /* title for browse dialog */ ) : /* constants for BrowseForFolder options */ &SCOPED BIF_RETURNONLYFSDIRS 1 &SCOPED BIF_DONTGOBELOWDOMAIN 2 DEFINE VARIABLE lv_chShell AS COM-HANDLE NO-UNDO. /* shell application */ DEFINE VARIABLE lv_chFolder AS COM-HANDLE NO-UNDO. /* holder for selected folder object */ DEFINE VARIABLE lv_cPathName AS CHARACTER NO-UNDO. /* folder pathame */ IF ip_cTitle EQ "":U OR ip_cTitle EQ ? THEN ASSIGN ip_cTitle = "Select Folder". /* create Shell Automation object */ CREATE "Shell.Application":U lv_chShell NO-ERROR. IF NOT VALID-HANDLE(lv_chShell) THEN RETURN "":u. /* automation object not present on system */ /* execute the browseForFolderMethod */ lv_chFolder = lv_chShell:BrowseForFolder(CURRENT-WINDOW:HWND, ip_cTitle, {&BIF_DONTGOBELOWDOMAIN} + {&BIF_RETURNONLYFSDIRS}). /* see if user has selected a valid folder */ IF VALID-HANDLE(lv_chFolder) AND lv_chFolder:SELF:IsFolder THEN ASSIGN lv_cPathName = lv_chFolder:SELF:Path. ELSE ASSIGN lv_cPathName = "":U. /* always release com objects when done */ RELEASE OBJECT lv_chFolder NO-ERROR. RELEASE OBJECT lv_chShell NO-ERROR. RETURN lv_cPathName. END FUNCTION.
Cyril O'Floinn has wrapped function SHBrowseForFolder into a higher-level DLL function, named BrowseForFolder. This has the advantage of being able to specify an initial directory.
The source for this new function is written in Delphi and is added to PROEXTRA.DLL.
The declaration is added to PROEXTRA.P and looks like this:
PROCEDURE BrowseForFolder EXTERNAL {&ProExtra} : DEFINE INPUT PARAMETER hWndOwner AS LONG. DEFINE INPUT PARAMETER lpTitle AS CHARACTER. DEFINE INPUT PARAMETER uiFlags AS LONG. DEFINE INPUT PARAMETER lpInitialFolder AS CHARACTER. DEFINE OUTPUT PARAMETER lpFolder AS CHARACTER. DEFINE RETURN PARAMETER BoolRetVal AS LONG. END PROCEDURE.
PROEXTRA.DLL and PROEXTRA.P are part of 'everything.zip' November 29, 1998 and can be downloaded from page windows.i and hpApi.
Cyril also made an example procedure, demonstrating the options of this function. This example is attached.
folder.w.zip : example
by Todd G. Nist
Program source is available for download: w-createshortcut.p
This program demonstrates how to create a shortcut in Windows and how to modify it. More specifically, the demo will create a folder called "SomeApplication", place a link to a "Readme.txt" file into the folder created, add it to the end users desktop, and create a link to this website under "Favorites".
By using the SHAddToRecentDocs() call, a shortcut can be created on the fly and added to the user's Documents item on the Start menu. Then by using the SHFileOperation function, one can move and rename files or folders across drives. By applying the SHGetSpecialFolderLocation API, we can get the current user's "Special" systems paths - their Start Menu, their Recent Files path, and others.
The code uses SHAddToRecentDocs to create shortcuts, SHGetSpecialFolderLocation to find the appropriate folders for the system, and SHFileOperation to create a folder, move and then rename the shortcuts. The end result is the ability to create any shortcut you want, to any file you want, and place it anywhere on the user's system. The example code does not do all the necessary error checking so if run more then once, you may encounter errors.
API-procedures used in this example are listed here to be included in the search index: PROCEDURE SHAddToRecentDocs EXTERNAL "shell32.dll" : PROCEDURE SHFileOperationA EXTERNAL "shell32.dll" : PROCEDURE SHGetPathFromIDListA EXTERNAL "shell32.dll" : PROCEDURE SHGetSpecialFolderLocation EXTERNAL "shell32.dll" : PROCEDURE SHChangeNotify EXTERNAL "shell32.dll" : PROCEDURE CoTaskMemFree EXTERNAL "ole32.dll" : PROCEDURE GetTempPathA EXTERNAL "kernel32.dll" : PROCEDURE Sleep EXTERNAL "kernel32.dll" : PROCEDURE FindClose EXTERNAL "kernel32.dll" : PROCEDURE FindFirstFileA EXTERNAL "kernel32.dll" :
w-createshortcut.p.zip : example
Hi there,
Working with an NTFS based operating system, I'd like to link so-called "File Summary Information" with our R-Codes (and get the "Summary" tab when accessing the "properties" of a R-Code in the Windows Explorer... just like .DOC, .XLS,... files).
I've seen that we have to use these functions to achieve this:
stgCreateStorageEx (to create a "File Summary Information" structure);
stgOpenStorageEx (to access the structure);
SetFileSummaryInfo (to fill the structure with info).
Does anyone have yet "played" with these functions, or have any advice or sample I can use to set this up?
Thanks in advance,
This code was posted to PEG by Jared Middleton.
/******************************************************************* Procedure: shellfile.p Description: Demo program to access File Summary Properties using Shell FolderItems object GetDetailsOf method. Written by: Jared Middleton (January 17, 2007) *******************************************************************/ DEFINE VAR chApp AS COM-HANDLE NO-UNDO. DEFINE VAR chFolder AS COM-HANDLE NO-UNDO. DEFINE VAR chFolderItem AS COM-HANDLE NO-UNDO. DEFINE VAR cFile AS CHARACTER NO-UNDO. DEFINE VAR cDir AS CHARACTER NO-UNDO. DEFINE VAR cBase AS CHARACTER NO-UNDO. DEFINE VAR lOpen AS LOGICAL NO-UNDO. DEFINE VAR cLabel AS CHARACTER NO-UNDO FORMAT "x(24)". DEFINE VAR cValue AS CHARACTER NO-UNDO FORMAT "x(50)". DEFINE FRAME frmDisp cLabel cValue WITH NO-LABELS DOWN. SYSTEM-DIALOG GET-FILE cFile FILTERS 'All Files (*.*)' '*.*', 'MP3 Files (*.mp3)' '*.mp3', /* optional */ 'JPG Files (*.jpg,*.jpeg)' '*.jpg,*.jpeg' /* optional */ UPDATE lOpen. IF NOT lOpen THEN RETURN. CREATE "Shell.Application" chApp NO-ERROR. IF VALID-HANDLE(chApp) THEN DO: ASSIGN cDir = SUBSTRING(cFile,1,R-INDEX(cFile,"\") - 1) cBase = SUBSTRING(cFile,LENGTH(cDir) + 2). ASSIGN chFolder = chApp:Namespace(cDir). IF VALID-HANDLE(chFolder) THEN DO: ASSIGN chFolderItem = chFolder:ParseName(cBase). /* Extended File Properties are OS version specific. The following are valid for Windows XP/2003 */ IF VALID-HANDLE(chFolderItem) THEN DO: RUN get-item ("File:",0). RUN get-item ("Size:",1). RUN get-item ("Type:",2). RUN get-item ("Title:",10). RUN get-item ("Subject:",11). RUN get-item ("Category:",12). RUN get-item ("Artist:",16). RUN get-item ("Album:",17). RUN get-item ("Year:",18). RUN get-item ("Track:",19). RUN get-item ("Genre:",20). RUN get-item ("Duration:",21). RUN get-item ("Bit Rate:",22). /*RUN get-item ("Protected:",23).*/ /*RUN get-item ("Episode Name:",29).*/ /*RUN get-item ("Audio Sample Size:",32).*/ RUN get-item ("Audio Sample Rate:",33). RUN get-item ("Channels:",34). RUN get-item ("Camera Model:",24). RUN get-item ("Date Taken:",25). /*RUN get-item ("Dimensions:",26).*/ RUN get-item ("Width:",27). RUN get-item ("Height:",28). END. END. END. RELEASE OBJECT chFolderItem NO-ERROR. RELEASE OBJECT chFolder NO-ERROR. RELEASE OBJECT chApp NO-ERROR. PROCEDURE get-item: DEFINE INPUT PARAMETER l-cLabel AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER l-cItem AS INTEGER NO-UNDO. DEFINE VAR l-cValue AS CHARACTER NO-UNDO. DO WITH FRAME frmDisp: ASSIGN l-cValue = chFolder:GetDetailsOf(chFolderItem,l-cItem). IF l-cValue > "" THEN DO: DISPLAY l-cLabel @ cLabel l-cValue @ cValue. DOWN. END. END. END PROCEDURE. /* End of program */
Most win32 binaries, like executables (exe), dynamic link libraries (dll) and automation controls (ocx) contain a version information structure. The data in this version information structure is used by setup programs to decide if it's ok to overwrite the file.
The version information can also be useful for support engineers for determining why a feature doesn't work as expected. Especially when used in a list of all modules loaded by the current Progress process, see ListModules.
Typically a file contains both a productversion and fileversion. The version info structure may also contain strings describing the file or its publisher, but this textual information is kind of difficult to read because they are grouped together in codepage blocks. Productversion and fileversion are not language specific so these are easier to read.
DEFINE VARIABLE vProductVersion AS CHARACTER NO-UNDO. DEFINE VARIABLE vFileVersion AS CHARACTER NO-UNDO. RUN GetProductVersion ( 'c:\windows\system\mfc42.dll', OUTPUT vProductVersion, OUTPUT vFileVersion). MESSAGE vProductVersion SKIP vFileVersion VIEW-AS ALERT-BOX. /* shows 6.0.1.0 6.0.8267.0 on my system. This indicates someone or something installed the runtime modules for MS Visual Studio 6 */
PROCEDURE GetProductVersion : DEFINE INPUT PARAMETER pFilename AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER pProductVersion AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER pFileVersion AS CHARACTER NO-UNDO. DEFINE VARIABLE dummy AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE lpVersionInfo AS MEMPTR NO-UNDO. /* VS_VERSION_INFO structure */ DEFINE VARIABLE lpFixedFileInfo AS MEMPTR NO-UNDO. /* VS_FIXEDFILEINFO structure */ DEFINE VARIABLE versize AS INTEGER NO-UNDO. /* size of lpVersionInfo */ DEFINE VARIABLE ptrInfo AS INTEGER NO-UNDO. /* address of lpFixedFileInfo */ DEFINE VARIABLE cInfo AS INTEGER NO-UNDO. /* size of lpFixedFileInfo */ RUN GetFileVersionInfoSizeA (pFileName, OUTPUT dummy, OUTPUT versize). IF versize = 0 THEN RETURN. SET-SIZE(lpVersionInfo) = 0. SET-SIZE(lpVersionInfo) = versize. RUN GetFileVersionInfoA ( pFileName, 0, INPUT versize, INPUT GET-POINTER-VALUE(lpVersionInfo), OUTPUT returnvalue). IF returnvalue = 0 THEN DO: SET-SIZE(lpVersionInfo) = 0. RETURN. END. RUN VerQueryValueA (GET-POINTER-VALUE(lpVersionInfo), "\":U, OUTPUT ptrInfo, OUTPUT cInfo, OUTPUT returnvalue). IF NOT (returnvalue=0 OR cInfo=0) THEN DO: SET-SIZE(lpFixedFileInfo) = cInfo. SET-POINTER-VALUE(lpFixedFileInfo) = ptrInfo. pProductVersion = STRING(GET-SHORT (lpFixedFileInfo,19)) + '.' + STRING(GET-SHORT (lpFixedFileInfo,17)) + '.' + STRING(GET-SHORT (lpFixedFileInfo,23)) + '.' + STRING(GET-SHORT (lpFixedFileInfo,21)). pFileVersion = STRING(GET-SHORT (lpFixedFileInfo,11)) + '.' + STRING(GET-SHORT (lpFixedFileInfo, 9)) + '.' + STRING(GET-SHORT (lpFixedFileInfo,15)) + '.' + STRING(GET-SHORT (lpFixedFileInfo,13)). END. SET-SIZE (lpVersionInfo) = 0. /* ------ DON'T DO THIS: -------- SET-SIZE (lpFixedFileInfo) = 0. */ END PROCEDURE.
Definitions used in this procedure:
PROCEDURE GetFileVersionInfoSizeA EXTERNAL "version.dll" : DEFINE INPUT PARAMETER lptstrFilename AS CHARACTER. DEFINE OUTPUT PARAMETER lpdwHandle AS LONG. DEFINE RETURN PARAMETER VersionInfoSize AS LONG. END PROCEDURE. PROCEDURE GetFileVersionInfoA EXTERNAL "version.dll" : DEFINE INPUT PARAMETER lptstrFilename AS CHARACTER. DEFINE INPUT PARAMETER dwHandle AS LONG. DEFINE INPUT PARAMETER dwLen AS LONG. DEFINE INPUT PARAMETER lpData AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE VerQueryValueA EXTERNAL "version.dll" : DEFINE INPUT PARAMETER lpBlock AS LONG. DEFINE INPUT PARAMETER lpSubBlock AS CHARACTER. DEFINE OUTPUT PARAMETER lplpBuffer AS LONG. DEFINE OUTPUT PARAMETER puLen AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.
by Stuart Morris
This allows you to call a standard system dialog for formatting Floppy disks and Hard drives etc.
/* S.A.Morris - 01/02/2000 */ &GLOB SHFD_CAPACITY_DEFAULT 0 /* default drive capacity */ &GLOB SHFD_CAPACITY_360 3 /* 360KB, applies to 5.25" drives only */ &GLOB SHFD_CAPACITY_720 5 /* 720KB, applies to 3.5" drives only */ &GLOB SHFD_FORMAT_QUICK 0 /* quick format */ &GLOB SHFD_FORMAT_FULL 1 /* full format */ &GLOB SHFD_FORMAT_SYSONLY 2 /* copies system files only (Win95 Only!) */ DEFINE VARIABLE RESULT AS INTEGER NO-UNDO. PROCEDURE SHFormatDrive EXTERNAL "shell32.dll" : DEFINE INPUT PARAM hwndOwner AS LONG. DEFINE INPUT PARAM iDrive AS LONG. DEFINE INPUT PARAM iCapacity AS LONG. DEFINE INPUT PARAM iFormatType AS LONG. DEFINE OUTPUT PARAM lpResult AS LONG. END PROCEDURE. RUN SHFormatDrive (INPUT CURRENT-WINDOW:HWND, 0, /* Drive A=0, B=1 (if present, otherwise C=1 etc) */ {&SHFD_CAPACITY_DEFAULT}, {&SHFD_FORMAT_QUICK}, OUTPUT RESULT ) NO-ERROR. /* Needs this to stop Stack Errors */
function GetShortPathName retrieves the 8.3 pathname for an existing long pathname.
PROCEDURE GetShortPathNameA EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER lpszLongPath AS CHARACTER. DEFINE OUTPUT PARAMETER lpszShortPath AS CHARACTER. DEFINE INPUT PARAMETER cchBuffer AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. DEFINE VARIABLE longname AS CHARACTER NO-UNDO. DEFINE VARIABLE shortname AS CHARACTER NO-UNDO. DEFINE VARIABLE returnvalue AS INTEGER NO-UNDO. &GLOB shortsize 68 longname = "C:\Program Files\VendorName\Some Application\Data\Monthly Revenue.txt". shortname = FILL("-", {&shortsize}). RUN GetShortPathNameA (longname, OUTPUT shortname, LENGTH(shortname), OUTPUT ReturnValue). IF ReturnValue > {&shortsize} THEN MESSAGE "buffer too short, specify at least " ReturnValue. ELSE IF ReturnValue = 0 THEN MESSAGE "file does not exist". ELSE shortname = ENTRY(1, shortname, CHR(0)).
MESSAGE GetSpecialFolder({&CSIDL_SYSTEM}) VIEW-AS ALERT-BOX.
will return "c:\windows\system" or "c:\winnt\system32" or whatever is valid on your PC.
There are different implementations of this function. This page shows one by Stuart Morris and one by Jan Verley.
by Stuart Morris, stuart@IBS-PUBLIC-SERVICES.co.uk
PROCEDURE SHGetPathFromIDListA EXTERNAL "shell32.dll":U : DEFINE INPUT PARAMETER pidl AS LONG. DEFINE OUTPUT PARAMETER pszPath AS CHARACTER. DEFINE RETURN PARAMETER iResult AS LONG. END PROCEDURE. PROCEDURE SHGetSpecialFolderLocation EXTERNAL "shell32.dll":U : DEFINE INPUT PARAMETER hwndOwner AS LONG. DEFINE INPUT PARAMETER nFolder AS LONG. DEFINE OUTPUT PARAMETER pidl AS LONG. DEFINE RETURN PARAMETER iResult AS LONG. END PROCEDURE. PROCEDURE CoTaskMemFree EXTERNAL "ole32.dll":U : DEFINE INPUT PARAMETER lpPidl AS LONG. END PROCEDURE. FUNCTION GetSpecialFolder RETURNS CHARACTER (INPUT iCSIDL AS INT): /*----------------------------------------------------------- Purpose: Notes: -------------------------------------------------------------*/ DEFINE VARIABLE iResult AS INTEGER NO-UNDO. DEFINE VARIABLE cPath AS CHARACTER NO-UNDO. DEFINE VARIABLE pidl AS INTEGER NO-UNDO. RUN SHGetSpecialFolderLocation (INPUT 0, INPUT iCSIDL, OUTPUT pidl, OUTPUT iResult ). IF iResult = 0 THEN DO: cPath = FILL(' ', {&MAX_PATH}). RUN SHGetPathFromIDListA(INPUT pidl, OUTPUT cPath, OUTPUT iResult ). RUN CoTaskMemFree(INPUT pidl). IF iResult GT 0 THEN RETURN TRIM(cPath) + '\':U. END. RETURN "". /* Function return value. */ END FUNCTION.
by Jan Verley, jverle@softcell.be
Jan made a similar function, using SHGetFolderPathA in shfolder.dll. SHGetFolderPathA is more programmer-friendly because it simply returns the path as a string, not as as pidl. SHGetFolderPathA is a wrapper to lower-level procedures, it selects the appropriate procedures matching the current Windows version. Another interesting aspect is that this procedure lets you impersonate any user to get his/her personal folders. The downside is that SHGetFolderPathA doesn't support every defined CSIDL.
Info (for Visual Basic programmers) about this on: http:www.mvps.org/vbnet/code/shell/csidl.htm and on http:developer.earthweb.com/journal/techworkshop/020400_vbwin2k.html
Be careful: shfolder.dll may not be available on your target machine. It is a freely redistributable dll and ships with Win2000 and NT4 service pack 4 and later, IE5, Win98 Second Edition and probably ships with other MS products as well.
PROCEDURE SHGetFolderPathA EXTERNAL "shfolder.dll":U : DEFINE INPUT PARAMETER hwndOwner AS LONG. DEFINE INPUT PARAMETER nFolder AS LONG. DEFINE INPUT PARAMETER hToken AS LONG. DEFINE INPUT PARAMETER dwFlags AS LONG. DEFINE OUTPUT PARAMETER pszPath AS CHARACTER. DEFINE RETURN PARAMETER hResult AS LONG. END PROCEDURE. FUNCTION GetSpecialFolder RETURNS CHARACTER (INPUT iCSIDL AS INT): /*----------------------------------------------------------- Purpose: Notes: -------------------------------------------------------------*/ DEFINE VARIABLE op_dir AS CHARACTER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. ASSIGN op_dir = FILL (" " ,{&MAX_PATH}). RUN SHGetFolderPathA (INPUT 0, INPUT iCSIDL, INPUT 0, INPUT 0, OUTPUT op_dir, OUTPUT ReturnValue). RETURN TRIM(op_dir). END FUNCTION.
Constants used in this example, and a few more. This is a subset from te list in [[winconst]] wikilink.zip.
&GLOBAL-DEFINE CSIDL_ADMINTOOLS 48 &GLOBAL-DEFINE CSIDL_ALTSTARTUP 29 &GLOBAL-DEFINE CSIDL_APPDATA 26 &GLOBAL-DEFINE CSIDL_BITBUCKET 10 &GLOBAL-DEFINE CSIDL_COMMON_ADMINTOOLS 47 &GLOBAL-DEFINE CSIDL_COMMON_ALTSTARTUP 30 &GLOBAL-DEFINE CSIDL_COMMON_DESKTOPDIRECTORY 25 &GLOBAL-DEFINE CSIDL_COMMON_DOCUMENTS 46 &GLOBAL-DEFINE CSIDL_COMMON_FAVORITES 31 &GLOBAL-DEFINE CSIDL_COMMON_PROGRAMS 23 &GLOBAL-DEFINE CSIDL_COMMON_STARTMENU 22 &GLOBAL-DEFINE CSIDL_COMMON_STARTUP 24 &GLOBAL-DEFINE CSIDL_COMMON_TEMPLATES 45 &GLOBAL-DEFINE CSIDL_CONTROLS 3 &GLOBAL-DEFINE CSIDL_COOKIES 33 &GLOBAL-DEFINE CSIDL_DESKTOP 0 &GLOBAL-DEFINE CSIDL_DESKTOPDIRECTORY 16 &GLOBAL-DEFINE CSIDL_DRIVES 17 &GLOBAL-DEFINE CSIDL_FAVORITES 6 &GLOBAL-DEFINE CSIDL_FONTS 20 &GLOBAL-DEFINE CSIDL_HISTORY 34 &GLOBAL-DEFINE CSIDL_INTERNET 1 &GLOBAL-DEFINE CSIDL_INTERNET_CACHE 32 &GLOBAL-DEFINE CSIDL_LOCAL_APPDATA 28 &GLOBAL-DEFINE CSIDL_MYPICTURES 39 &GLOBAL-DEFINE CSIDL_NETHOOD 19 &GLOBAL-DEFINE CSIDL_NETWORK 18 &GLOBAL-DEFINE CSIDL_PERSONAL 5 &GLOBAL-DEFINE CSIDL_PRINTERS 4 &GLOBAL-DEFINE CSIDL_PRINTHOOD 27 &GLOBAL-DEFINE CSIDL_PROFILE 40 &GLOBAL-DEFINE CSIDL_PROGRAMS 2 &GLOBAL-DEFINE CSIDL_PROGRAM_FILES 38 &GLOBAL-DEFINE CSIDL_PROGRAM_FILESX86 42 &GLOBAL-DEFINE CSIDL_PROGRAM_FILES_COMMON 43 &GLOBAL-DEFINE CSIDL_PROGRAM_FILES_COMMONX86 44 &GLOBAL-DEFINE CSIDL_RECENT 8 &GLOBAL-DEFINE CSIDL_SENDTO 9 &GLOBAL-DEFINE CSIDL_STARTMENU 11 &GLOBAL-DEFINE CSIDL_STARTUP 7 &GLOBAL-DEFINE CSIDL_SYSTEM 37 &GLOBAL-DEFINE CSIDL_SYSTEMX86 41 &GLOBAL-DEFINE CSIDL_TEMPLATES 21 &GLOBAL-DEFINE CSIDL_WINDOWS 36 &GLOBAL-DEFINE CSIDL_FLAG_CREATE 32768 &GLOBAL-DEFINE CSIDL_FLAG_DONT_VERIFY 16384 &GLOBAL-DEFINE CSIDL_FLAG_MASK 65280 &GLOBAL-DEFINE MAX_PATH 260
This page explains how to find the size (in bytes) of a certain file, the long and short filenames, the date and time a file was last modified. It also shows a way to get a directory listing.
This is based on a rather large procedure library, file-api.p and file-api.i, which are available in everything.zip in page windows.i and hpApi
To use any of the functions in library file-api.p simply include {file-api.i} in the definitions section of your program. This will define some constants you might want to use and it runs file-api.p persistent in handle hpFileApi.
The library file-api.p defines a couple of API functions but you will probably not run these directly. Instead, you will probably run one of the 4GL internal procedures in there.
The procedures work with a memptr to a WIN32_FIND_DATA structure. So the procedures will either have an output parameter or an input parameter of this type. You don't have to allocate or fill this structure or fetch information from it; this is all done by the procedures themselves.
Here's an example of getting information about file "c:\autoexec.bat" :
{file-api.i} DEFINE VARIABLE lpFindData AS MEMPTR. DEFINE VARIABLE longname AS CHARACTER. DEFINE VARIABLE shortname AS CHARACTER. DEFINE VARIABLE SIZE AS INTEGER. DEFINE VARIABLE chDate AS DATE. DEFINE VARIABLE chTime AS INTEGER. /* get a lpFindData structure */ RUN FileFind IN hpFileApi ("c:\autoexec.bat", OUTPUT lpFindData). /* read information from the lpFileInfo structure */ RUN FileInfo_LongName IN hpFileApi(lpFindData, OUTPUT longname). RUN FileInfo_ShortName IN hpFileApi(lpFindData, OUTPUT shortname). RUN FileInfo_Size IN hpFileApi(lpFindData, OUTPUT SIZE). RUN FileInfo_LastWrite IN hpFileApi(lpFindData, OUTPUT chDate, OUTPUT chTime). MESSAGE "name=" longname SKIP "short name=" shortname SKIP "modified=" chDate STRING(chTime,"hh:mm:ss") SKIP "size=" SIZE " bytes" VIEW-AS ALERT-BOX.
The general idea is: find the first file in the directory and repeat to find the next file until no more files are found. This loop is performed by the procedure FileFindLoop in file-api.p. Whenever it finds a file, it runs an internal procedure you specified with an lpFindData pointer as input parameter. Example:
{file-api.i} RUN FileFindLoop IN hpFileApi ("d:\progress\*.p", /* mask, must contain wildcards */ "ProcessOneFile", /* name of callback procedure */ THIS-PROCEDURE:HANDLE). /* location of callback proc */ PROCEDURE ProcessOneFile : DEFINE INPUT PARAMETER lpFindData AS MEMPTR. /* do whatever you like here, for example show the file name if modified within last 3 days */ DEFINE VARIABLE longname AS CHARACTER NO-UNDO. DEFINE VARIABLE chDate AS DATE NO-UNDO. DEFINE VARIABLE chTime AS INTEGER NO-UNDO. RUN FileInfo_LongName IN hpFileApi(lpFindData, OUTPUT longname). RUN FileInfo_LastWrite IN hpFileApi(lpFindData, OUTPUT chDate, OUTPUT chTime). IF chDate> TODAY - 3 THEN MESSAGE longname VIEW-AS ALERT-BOX. END PROCEDURE.
The callback procedure in the above example will be called for every file that meets the mask, these can include files or (sub)directories. Often you will want to show only directories, or only files, or only files that have the Archive-bit set, or skip all hidden and system files. Whatever. In those cases you will need to test the file attributes during the callback function.
File attributes are one DWORD where each bit represents one certain attribute. The attributes are stored in the first element of the lpFindData structure, the meaning of the different bits is listed in file-api.i for your convenience. To test for the presence of a certain bit you must use binary logic (the AND), covered on page "Bitwise operators using ProExtra.DLL".
So if you want to make sure if a file is actually a directory, you would include a test like this:
{file-api.i} PROCEDURE ProcessOneFile : DEFINE INPUT PARAMETER lpFindData AS MEMPTR. DEFINE VARIABLE attribs AS INTEGER NO-UNDO. DEFINE VARIABLE RESULT AS INTEGER NO-UNDO. attribs = GET-LONG(lpFindData,1). RUN Bit_And IN hpExtra(attribs, {&DDL_DIRECTORY}, OUTPUT RESULT). IF result NE 0 THEN DO: /* whatever you want to do with a directory */ END. ELSE DO: /* whatever you want to do with a file */ END. END PROCEDURE.
Note that flag DDL_READWRITE has value 0. This is a brain teaser: it can't be set, so you can't test for its presence! The definition says: a file is READWRITE unless any other bit is set. In other words: you don't use Bit_And() to test if attribs contains DDL_READWRITE, but you simply test if attribs=DDL_READWRITE.
by Michael Rüsweg-Gilbert
Procedure WNetGetConnection retrieves the name of the network resource associated with a local device. In other words, it can be used to get the UNC name for a mapped network drive or printer. If K: is the drive letter of a mapped network drive, the following example finds the UNC name for this network drive.
DEFINE VARIABLE Drive_Name AS CHARACTER NO-UNDO INIT "K:". DEFINE VARIABLE UNC_Name AS CHARACTER NO-UNDO. DEFINE VARIABLE namelen AS INTEGER NO-UNDO INITIAL 100. DEFINE VARIABLE retBool AS INTEGER NO-UNDO. UNC_Name = FILL("x",namelen). RUN WNetGetConnectionA ( Drive_Name, OUTPUT UNC_Name, INPUT-OUTPUT namelen, OUTPUT retBool). IF retBool = 0 THEN UNC_Name = SUBSTRING(UNC_Name, 1, namelen). ELSE UNC_Name = "". MESSAGE UNC_Name VIEW-AS ALERT-BOX.
API definitions used in this exampe, not listed in windows.p :
PROCEDURE WNetGetConnectionA EXTERNAL "mpr.dll" : DEFINE INPUT PARAMETER lpDrive AS CHARACTER. DEFINE OUTPUT PARAMETER lpUNCName AS CHARACTER. DEFINE INPUT-OUTPUT PARAMETER lpnLength AS LONG. DEFINE RETURN PARAMETER RetBool AS LONG. END PROCEDURE.
Code found in an e-mail to Peg, send by by Jeffrey L. Boyer
Sometimes you want to read a file and be sure that nobody else is writing to the file in the meantime. So you want to lock it for writing.
&GLOBAL-DEFINE GENERIC_WRITE 1073741824 /* &H40000000 */ &GLOBAL-DEFINE OPEN_EXISTING 3 &GLOBAL-DEFINE FILE_SHARE_READ 1 /* = &H1 */ &GLOBAL-DEFINE FILE_ATTRIBUTE_NORMAL 128 /* = &H80 */ PROCEDURE CreateFileA EXTERNAL "kernel32": DEFINE INPUT PARAMETER lpFileName AS CHARACTER. DEFINE INPUT PARAMETER dwDesiredAccess AS LONG. DEFINE INPUT PARAMETER dwShareMode AS LONG. DEFINE INPUT PARAMETER lpSecurityAttributes AS LONG. DEFINE INPUT PARAMETER dwCreationDisposition AS LONG. DEFINE INPUT PARAMETER dwFlagsAndAttributes AS LONG. DEFINE INPUT PARAMETER hTemplateFile AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE CloseHandle EXTERNAL "kernel32" : DEFINE INPUT PARAMETER hObject AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. DEFINE VARIABLE lpSecurityAtt AS INTEGER NO-UNDO. DEFINE VARIABLE hObject AS INTEGER NO-UNDO. DEFINE VARIABLE nReturn AS INTEGER NO-UNDO. /* Lock file agains writing */ RUN CreateFileA (INPUT source.txt, INPUT {&GENERIC_WRITE}, {&FILE_SHARE_READ}, lpSecurityAtt, {&OPEN_EXISTING}, {&FILE_ATTRIBUTE_NORMAL}, 0, OUTPUT hObject). input from source.txt. repeat: import aline. /* do stuff */ end. input close. /* Release file handle */ RUN CloseHandle (INPUT hObject, OUTPUT nReturn).
Let's start with a couple of definitions:
&GLOBAL-DEFINE FILE_ATTRIBUTE_READONLY 1 &GLOBAL-DEFINE FILE_ATTRIBUTE_HIDDEN 2 &GLOBAL-DEFINE FILE_ATTRIBUTE_SYSTEM 4 &GLOBAL-DEFINE FILE_ATTRIBUTE_DIRECTORY 16 &GLOBAL-DEFINE FILE_ATTRIBUTE_ARCHIVE 32 &GLOBAL-DEFINE FILE_ATTRIBUTE_NORMAL 128 &GLOBAL-DEFINE FILE_ATTRIBUTE_COMPRESSED 2048 PROCEDURE SetFileAttributesA EXTERNAL "kernel32" : DEFINE INPUT PARAMETER lpFilename AS CHARACTER. DEFINE INPUT PARAMETER dwFileAttributes AS LONG. END. PROCEDURE GetFileAttributesA EXTERNAL "kernel32" : DEFINE INPUT PARAMETER lpFilename AS CHARACTER. DEFINE RETURN PARAMETER dwFileAttributes AS LONG. END.
The first example shows how to make a file read-only. It also clears most of the existing attributes because only one attribute is specified.
RUN SetFileAttributesA ( "c:\autoexec.bat", {&FILE_ATTRIBUTE_READONLY} ).
(Specify FILE_ATTRIBUTE_NORMAL to clear all attributes.)
The next example makes the file read-only and leaves the existing attributes intact.
DEFINE VARIABLE lv_attribs AS INTEGER NO-UNDO. RUN GetFileAttributesA ( "c:\autoexec.bat", OUTPUT lv-attribs ). /* if lv-attribs does not include {&FILE_ATTRIBUTE_READONLY} then */ RUN SetFileAttributesA ( "c:\autoexec.bat", lv-attribs + {&FILE_ATTRIBUTE_READONLY}).
The commented line should be replaced by an actual IF-statement. You can use procedure CheckOneAttribute by Dmitri, or a variant of his procedure, to test if the file already has the attribute set.
/* by Dmitri Levin, dlevin@ryland.com */ DEFINE VARIABLE lv-filename AS CHARACTER INIT "c:\autoexec.bat" NO-UNDO. DEFINE VARIABLE lv-attribs AS INTEGER NO-UNDO. DEFINE VARIABLE lv-attribs-list AS CHARACTER NO-UNDO. RUN GetFileAttributesA ( lv-filename, OUTPUT lv-attribs ). RUN CheckOneAttribute( lv-attribs, {&FILE_ATTRIBUTE_READONLY}, "READONLY", INPUT-OUTPUT lv-attribs-list). RUN CheckOneAttribute( lv-attribs, {&FILE_ATTRIBUTE_HIDDEN}, "HIDDEN", INPUT-OUTPUT lv-attribs-list). RUN CheckOneAttribute( lv-attribs, {&FILE_ATTRIBUTE_SYSTEM}, "SYSTEM", INPUT-OUTPUT lv-attribs-list). RUN CheckOneAttribute( lv-attribs, {&FILE_ATTRIBUTE_DIRECTORY}, "DIRECTORY", INPUT-OUTPUT lv-attribs-list). RUN CheckOneAttribute( lv-attribs, {&FILE_ATTRIBUTE_ARCHIVE}, "ARCHIVE", INPUT-OUTPUT lv-attribs-list). MESSAGE "File Attributes:" lv-attribs-list VIEW-AS ALERT-BOX. PROCEDURE CheckOneAttribute : DEFINE INPUT PARAMETER lp-attribs AS INTEGER NO-UNDO. DEFINE INPUT PARAMETER lp-attrib-num AS INTEGER NO-UNDO. DEFINE INPUT PARAMETER lp-attrib-name AS CHARACTER NO-UNDO. DEFINE INPUT-OUTPUT PARAMETER lp-attrib-list AS CHARACTER NO-UNDO. IF lp-attribs MOD ( lp-attrib-num * 2 ) GE lp-attrib-num THEN lp-attrib-list = lp-attrib-list + MIN(lp-attrib-list,", ") + lp-attrib-name. END.
by Simon Sweetman
This is a demo program that uses SHAutoComplete from shlwapi.dll to enable the Windows filename auto complete (like the Win2K file-open dialog) on an editor field, the same call can be used on a fill-in but it doesn?t work real well with progress. The editor version works fine except for TAB and BACK-TAB which seem to be trapped by SHAutoComplete before they get to progress.
by Scott Anderson, Stuart Morris and Jurjen
"SYSTEM-DIALOG GET-FILE" allows to select only one filename. When you want to select multiple filenames you can call API function GetOpenFileNameA, specifying the OFN_ALLOWMULTISELECT flag.
Procedure SelectMultipleFileNames uses GetOpenFileNameA for the purpose of selecting multiple filenames. The first listing shows how to call the procedure, the second listing shows the implementation of the procedure.
The parameters are:
* FilterList
a list of filters separated by 'pipe'-symbols ( "|" ). Each individual filter is a description, followed by a pipe-symbol, followed by a semicolon-separated list of wildcards. The format of the description is not important but by convention it should be a text followed by the list of wildcards between brackets. The first filter is the default.
* InitialDirectory
Name of the directory where you want to the dialog to start. Specify the unknown value (?) to start in the current directory.
* DialogTitle
Specifies the title for the dialog.
* FileNames
returns a comma-separated list of selected filenames unless OK=FALSE.
* OK
returns FALSE if the user selected the Cancel button.
DEFINE VARIABLE lv-Files AS CHARACTER NO-UNDO. DEFINE VARIABLE OK AS LOGICAL NO-UNDO. RUN SelectMultipleFileNames (INPUT "Word Documents (*.doc,*.rtf)|*.doc;*.rtf" + "|" + "Excel Worksheets (*.xls)|*.xls" + "|" + "Access Databases (*.mdb)|*.mdb" + "|" + "All (doc,rtf,xls,mdb,ppt)|*.doc;*.rtf;*.xls;*.mdb;*.ppt", INPUT "C:\My Documents", INPUT "Select one or more Office documents", OUTPUT lv-Files, OUTPUT OK ). IF OK THEN MESSAGE "you selected these files:" SKIP lv-Files VIEW-AS ALERT-BOX. ELSE MESSAGE "you pressed Cancel" VIEW-AS ALERT-BOX.
&GLOBAL-DEFINE OFN_OVERWRITEPROMPT 2 &GLOBAL-DEFINE OFN_HIDEREADONLY 4 &GLOBAL-DEFINE OFN_NOCHANGEDIR 8 &GLOBAL-DEFINE OFN_ALLOWMULTISELECT 512 &GLOBAL-DEFINE OFN_PATHMUSTEXIST 2048 &GLOBAL-DEFINE OFN_FILEMUSTEXIST 4096 &GLOBAL-DEFINE OFN_NOREADONLYRETURN 32768 &GLOBAL-DEFINE OFN_EXPLORER 524288 PROCEDURE GetOpenFileNameA EXTERNAL "comdlg32.dll" : DEFINE INPUT PARAMETER lpOfn AS LONG. DEFINE RETURN PARAMETER pReturn AS LONG. END PROCEDURE. PROCEDURE SelectMultipleFileNames : /*------------------------------------------------------------------------------ Purpose: Replaces the SYSTEM-DIALOG-GET-FILE common dialog, supports multiselect. Parameters: ------------------------------------------------------------------------------*/ DEFINE INPUT PARAMETER FilterList AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER InitialDirectory AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER DialogTitle AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER FileNames AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER OK AS INTEGER NO-UNDO. DEFINE VARIABLE Flags AS INTEGER NO-UNDO. DEFINE VARIABLE lpOfn AS MEMPTR NO-UNDO. DEFINE VARIABLE lpstrFilter AS MEMPTR NO-UNDO. DEFINE VARIABLE lpstrTitle AS MEMPTR NO-UNDO. DEFINE VARIABLE lpstrInitialDir AS MEMPTR NO-UNDO. DEFINE VARIABLE lpstrFile AS MEMPTR NO-UNDO. DEFINE VARIABLE offset AS INTEGER NO-UNDO. /* Flags controls the behaviour and appearance of the dialog-box. There is much room for experiments. This combination works nice: */ Flags = {&OFN_ALLOWMULTISELECT} + {&OFN_EXPLORER} + {&OFN_NOCHANGEDIR}. /* convert the "|"-separated list of filters to a CHR(0)-separated list and make sure it's terminated with a double CHR(0): */ FilterList = TRIM(FilterList,"|") + "|". /* this will cause the double CHR(0) */ SET-SIZE(lpstrFilter) = LENGTH(FilterList) + 1. PUT-STRING(lpstrFilter, 1) = FilterList. DO offset=1 TO GET-SIZE(lpstrFilter) : IF GET-BYTE(lpstrFilter,offset)=124 /* =ASC("|") */ THEN PUT-BYTE(lpstrFilter,offset)=0. END. /* get memory-pointers to the string parameters: */ SET-SIZE(lpstrFile) = 1024. /* room for a couple of files... */ PUT-BYTE(lpstrFile,1) = 0. /* don't initialize dialog to a file */ SET-SIZE(lpstrTitle) = LENGTH(DialogTitle) + 1. PUT-STRING(lpstrTitle,1) = DialogTitle. IF InitialDirectory NE ? THEN DO: SET-SIZE(lpstrInitialDir) = LENGTH(InitialDirectory) + 1. PUT-STRING(lpstrInitialDir,1) = InitialDirectory. END. /* create and initialize an OPENFILENAME structure: */ SET-SIZE(lpOfn) = 76. /* = {&OPENFILENAME_SIZE_VERSION_400} to be used in NT4 and Windows 95/98. Windows 2000 supports a couple more fields. */ /* size */ PUT-LONG (lpOfn, 1) = GET-SIZE(lpOfn). /* hwndOwner */ PUT-LONG (lpOfn, 5) = CURRENT-WINDOW:HWND. /* hInstance */ PUT-LONG (lpOfn, 9) = 0. /* lpstrFilter */ PUT-LONG (lpOfn,13) = GET-POINTER-VALUE(lpstrFilter). /* lpstrCustomFilter */ PUT-LONG (lpOfn,17) = 0. /* nMaxCustFilter */ PUT-LONG (lpOfn,21) = 0. /* nFilterIndex */ PUT-LONG (lpOfn,25) = 0. /* lpstrFile */ PUT-LONG (lpOfn,29) = GET-POINTER-VALUE(lpstrFile). /* nMaxFile */ PUT-LONG (lpOfn,33) = GET-SIZE(lpstrFile). /* lpstrFileTitle */ PUT-LONG (lpOfn,37) = 0. /* nMaxFileTitle */ PUT-LONG (lpOfn,41) = 0. /* lpstrInitialDir */ PUT-LONG (lpOfn,45) = GET-POINTER-VALUE(lpstrInitialDir). /* lpstrTitle */ PUT-LONG (lpOfn,49) = GET-POINTER-VALUE(lpstrTitle). /* flags */ PUT-LONG (lpOfn,53) = Flags. /* nFileOffset */ PUT-SHORT(lpOfn,57) = 0. /* nFileExtension */ PUT-SHORT(lpOfn,59) = 0. /* lpstrDefExt */ PUT-LONG (lpOfn,61) = 0. /* lCustData */ PUT-LONG (lpOfn,65) = 0. /* lpfnHook */ PUT-LONG (lpOfn,69) = 0. /* lpTemplateName */ PUT-LONG (lpOfn,73) = 0. /* run the dialog: */ RUN GetOpenFileNameA (GET-POINTER-VALUE(lpOfn), OUTPUT OK). /* release memory: */ SET-SIZE(lpstrFilter) = 0. SET-SIZE(lpOfn) = 0. SET-SIZE(lpstrTitle) = 0. SET-SIZE(lpstrInitialDir) = 0. /* lpstrFilter now contains a path, followed by CHR(0), followed by a CHR(0)-separated list of filenames, terminated by a double CHR(0). Unless the user selected only one file: then lpstrFilter will simply contain the fully-qualified filename. Either way, let's convert the result to a comma-separated list of fully-qualified filenames: */ IF OK NE 0 THEN DO: DEFINE VARIABLE cPath AS CHARACTER NO-UNDO. DEFINE VARIABLE cList AS CHARACTER NO-UNDO. DEFINE VARIABLE cFile AS CHARACTER NO-UNDO. ASSIGN cPath = GET-STRING(lpstrFile,1) offset = LENGTH(cPath) + 2. REPEAT: cFile = GET-STRING(lpstrFile, offset). IF cFile = "" THEN LEAVE. ASSIGN cList = cList + ',' + cPath + '\' + cFile offset = offset + LENGTH(cFile) + 1. END. ASSIGN cList = TRIM(cList, ",") FileNames = IF cList = "" THEN cPath ELSE cList. END. SET-SIZE(lpstrFile) = 0. END PROCEDURE. /* SelectMultipleFileNames */
by Todd G. Nist
Program source is available for download: w-SHFileOp.p
By calling SHFileOperation, one can leverage the existing dialogs for moving files and providing user feed back as to the status of the process all with just one call. It is a fairly simple demo, where it will ask for a directory to be copied, accepting wild cards, a destination directory, a delete directory file specification and a title for the dialog box. Then by invoking the call to SHFileOperation the standard windows dialog box showing the folders and the flying documents will be displayed, this is making the assumption that the information being copied is large enough to allow the dialog to be created. Also, if the files already exist, it will bring up the standard dialog asking if you wish to over write, the size of the file and the date of the files in question. Finally, if delete files is chosen, it will remove the files from the "delete file spec" and bring up the same general dialogs.
To test just make sure you are coping a large file or directory structure. Once it has completed, choose "copy files" again and you should see all the standard dialogs. Finally, specify a delete file spec and choose "delete files".
API-procedures used in this example are listed here to be included in the search index: PROCEDURE SHFileOperationA EXTERNAL "Shell32.dll": PROCEDURE FormatMessageA EXTERNAL "kernel32.dll":
w-shfileop.p.zip : example
by Gordon Campbell
I created a test program that illustrates how to 'watch a folder' in MS Windows. The code can be downloaded at: http://www.epro-sys.com/samples/watchfolder.w The sample watches a directory called c:\watch\in and via an IP (ProcessFile) moves the contents to c:\watch\out. The ProcessFile IP can be modified to process a file in the 'in' directory. You could use this to automate conversion of text files to PDF via PDFinclude or automate the sending of documents to e-mail addresses .... or whatever automated process you would consider when dealing with a file. Later, Gordon Campbell WIKA Instruments Ltd.
This source code example uses the following Win32 API functions:
* FindFirstChangeNotificationA
* FindCloseChangeNotification
* FindNextChangeNotification
* WaitForSingleObject
Just to be on the safe side, I have also attached a copy of his source code:
watchfolder.w.zip : original from Gordon Campbell
.
WinHelp is history... HTML Help is the future. The API for HTML Help is implemented in hhctrl.ocx (don't let the filename fool you; the ocx contains both an ActiveX control and API functions). First let's take a look at the definition:
&GLOBAL-DEFINE HH_DISPLAY_TOPIC 0 &GLOBAL-DEFINE HH_KEYWORD_LOOKUP 13 &GLOBAL-DEFINE HH_DISPLAY_TEXT_POPUP 14 PROCEDURE HtmlHelpA EXTERNAL "hhctrl.ocx" PERSISTENT : DEFINE INPUT PARAMETER hwndCaller AS LONG. DEFINE INPUT PARAMETER pszFile AS CHARACTER. DEFINE INPUT PARAMETER uCommand AS LONG. DEFINE INPUT PARAMETER dwData AS LONG. DEFINE RETURN PARAMETER hwndHelp AS LONG. END PROCEDURE.
Notice the **PERSISTENT** keyword: it is required for this function!
hwndCaller is the HWND of the calling window. The help-window stays on top of this calling window. You can specify hwndCaller=0 if you don't like that stay-on-top behaviour.
pszFile is (depending on uCommand) the name of the helpfile, combined with the name of the topic file, combined with the name of the window class in which you want to show the topic. For example "apisite.chm::/playsounda.html>mainwin".
pszFile can also be used to specify the text to show in a popup-window (if uCommand is HH_DISPLAY_TEXT_POPUP) as demonstrated on the next page, Popup windows from HTML Help
pszFile can also be NULL for some values of uCommand. That is going to be a problem in the way I defined the function using a CHAR parameter.
uCommand specifies what the function is supposed to do; examples follow.
dwData depends on uCommand, it is most often a pointer to some structure or simply NULL.
hwndHelp (the return parameter) is the HWND of the created help window. It can be used for manipulating the help window from within your application.
The following example opens helpfile apisite.chm and navigates to a specific topic file (playsounda.html).
RUN ShowHelpTopic ( FRAME {&FRAME-NAME}:HANDLE, "PlaySoundA"). PROCEDURE ShowHelpTopic : DEFINE INPUT PARAMETER hParent AS HANDLE NO-UNDO. DEFINE INPUT PARAMETER cTopic AS CHARACTER NO-UNDO. DEFINE VARIABLE hWndHelp AS INTEGER NO-UNDO. IF NOT VALID-HANDLE(hParent) THEN hParent = CURRENT-WINDOW:HANDLE. IF cTopic NE '' THEN cTopic = "::/" + cTopic + ".html". RUN HtmlHelpA( hParent:HWND , "apisite.chm" + cTopic, {&HH_DISPLAY_TOPIC}, 0, OUTPUT hWndHelp). END PROCEDURE.
The next example uses keyword lookup. The input parameter is a ";" separated list of keywords. Keyword lookup is case sensitive!! If exactly one matching topic is found it will immediately be shown. If more than one match is found you will get to see a menu.
What happens if no matches are found? Well, that depends on the members in the lpHH_AKLINK structure. This example is set to open the helpfile and show the "Index" tab. It could also have been set to go to a specific topic (using the pszUrl field) or to show a messagebox (using pszMsgText and pszMsgTitle)
RUN ShowHelpKeyword ( FRAME {&FRAME-NAME}:HANDLE, "BrowseForFolder;SHBrowseForFolder"). PROCEDURE ShowHelpKeyword : DEFINE INPUT PARAMETER hParent AS HANDLE NO-UNDO. DEFINE INPUT PARAMETER cKeywords AS CHARACTER NO-UNDO. DEFINE VARIABLE hWndHelp AS INTEGER NO-UNDO. DEFINE VARIABLE lpKeywords AS MEMPTR NO-UNDO. DEFINE VARIABLE lpHH_AKLINK AS MEMPTR NO-UNDO. IF cKeywords="" THEN RETURN. IF NOT VALID-HANDLE(hParent) THEN hParent = CURRENT-WINDOW:HANDLE. /* first use HH_DISPLAY_TOPIC to initialize the help window */ RUN ShowHelpTopic (hParent, ""). /* should really check if this succeeded.... */ /* if succeeded then use HH_KEYWORD_LOOKUP */ SET-SIZE (lpKeywords) = LENGTH(cKeywords) + 2. PUT-STRING(lpKeywords, 1) = cKeywords. SET-SIZE (lpHH_AKLINK) = 32. PUT-LONG (lpHH_AKLINK, 1) = GET-SIZE(lpHH_AKLINK). PUT-LONG (lpHH_AKLINK, 5) = INT(FALSE). /* reserved, always FALSE */ PUT-LONG (lpHH_AKLINK, 9) = GET-POINTER-VALUE(lpKeywords). PUT-LONG (lpHH_AKLINK,13) = 0. /* pszUrl */ PUT-LONG (lpHH_AKLINK,17) = 0. /* pszMsgText */ PUT-LONG (lpHH_AKLINK,21) = 0. /* pszMsgTitle */ PUT-LONG (lpHH_AKLINK,25) = 0. /* pszWindow */ PUT-LONG (lpHH_AKLINK,29) = INT(TRUE). /* fIndexOnFail */ RUN HtmlHelpA( hParent:HWND , "apisite.chm", {&HH_KEYWORD_LOOKUP}, GET-POINTER-VALUE(lpHH_AKLINK), OUTPUT hWndHelp). SET-SIZE (lpHH_AKLINK) = 0. SET-SIZE (lpKeywords) = 0. END PROCEDURE.
The previous topic calling HTML Help described how to call HtmlHelp to show a normal HTML topic. Here is how to use HtmlHelp to show a popup or contexthelp-window.
The declaration for the HtmlHelp function is now slightly different: pszFile is now declared as a LONG parameter because we will have to pass it the NULL value. This is the new declaration:
&GLOBAL-DEFINE HH_DISPLAY_TEXT_POPUP 14 PROCEDURE HtmlHelpA EXTERNAL "hhctrl.ocx" PERSISTENT : DEFINE INPUT PARAMETER hwndCaller AS LONG. DEFINE INPUT PARAMETER pszFile AS LONG. DEFINE INPUT PARAMETER uCommand AS LONG. DEFINE INPUT PARAMETER dwData AS LONG. DEFINE RETURN PARAMETER hwndHelp AS LONG. END PROCEDURE.
There are at least two different ways to create a popup. The first way does not need a CHM file: you can simply pass the string you want to show. The second way uses a CHM file which contains a list of strings.
Procedure HHPopupString in the following example creates a popup containing a specified string, positioned near a specified Progress widget.
{windows.i} RUN HHPopupString (FILL-IN-1:HANDLE, "This is the text that will be shown in the popup window"). PROCEDURE HHPopupString : DEFINE INPUT PARAMETER phWidget AS WIDGET-HANDLE NO-UNDO. DEFINE INPUT PARAMETER pText AS CHARACTER NO-UNDO. DEFINE VARIABLE FontSpec AS CHARACTER. DEFINE VARIABLE HH_POPUP AS MEMPTR. DEFINE VARIABLE lpText AS MEMPTR. DEFINE VARIABLE lpFont AS MEMPTR. DEFINE VARIABLE lpPoint AS MEMPTR. DEFINE VARIABLE retval AS INTEGER NO-UNDO. SET-SIZE (lpText) = LENGTH(pText) + 1. PUT-STRING(lpText,1) = pText. /* specify a font, format "facename[, point size[, charset[ BOLD ITALIC UNDERLINE]]]" */ FontSpec = 'MS Sans Serif,10,,BOLD'. FontSpec = 'MS Sans Serif,10,,'. SET-SIZE (lpFont) = LENGTH(FontSpec) + 1. PUT-STRING(lpFont,1) = FontSpec. /* screen coordinates. There is something weird about this */ /* I currently have only one disply monitor... should test this calculation with a secondary monitor using negative coords */ SET-SIZE (lpPoint) = 8. PUT-LONG (lpPoint,1) = INTEGER(phWidget:WIDTH-PIXELS / 2). PUT-LONG (lpPoint,5) = INTEGER(phWidget:HEIGHT-PIXELS / 2). RUN ClientToScreen IN hpApi(phWidget:HWND, GET-POINTER-VALUE(lpPoint), OUTPUT retval). /* fill the HH_POPUP structure */ SET-SIZE (HH_POPUP) = 52. PUT-LONG (HH_POPUP, 1) = GET-SIZE(HH_POPUP). PUT-LONG (HH_POPUP, 5) = 0. /* or hInstance for a DLL that contains string resource */ PUT-LONG (HH_POPUP, 9) = 0. /* or number of string resource in the DLL */ PUT-LONG (HH_POPUP,13) = GET-POINTER-VALUE(lpText). PUT-LONG (HH_POPUP,17) = GET-LONG(lpPoint,1). /* X-coordinate of center */ PUT-LONG (HH_POPUP,21) = GET-LONG(lpPoint,5). /* Y-coordinate of top */ PUT-LONG (HH_POPUP,25) = 1. /* default textcolor or RGB-VALUE(Red,Green,Blue) */ PUT-LONG (HH_POPUP,29) = -1. /* default bgcolor or RGB-VALUE(Red,Green,Blue) */ PUT-LONG (HH_POPUP,33) = -1. /* default left margin */ PUT-LONG (HH_POPUP,37) = -1. /* default top margin */ PUT-LONG (HH_POPUP,41) = -1. /* default right margin */ PUT-LONG (HH_POPUP,45) = -1. /* default bottom margin */ PUT-LONG (HH_POPUP,49) = 0. /* or get-pointer-value(lpFont). */ RUN HtmlHelpA ( phWidget:HWND, 0, {&HH_DISPLAY_TEXT_POPUP}, GET-POINTER-VALUE(HH_POPUP), OUTPUT RetVal). /* free memory */ SET-SIZE (lpText) = 0. SET-SIZE (lpFont) = 0. SET-SIZE (lpPoint) = 0. SET-SIZE (HH_POPUP) = 0. END PROCEDURE.
As you can see in you can manipulate colors, fonts, margins and coordinates but I only used values that represent the system defaults.
The next example uses a CHM file to retrieve the string. A single CHM file can contain many strings, each string has a ContextId number.
{windows.i} RUN HHPopupContext (FILL-IN-1:HANDLE, "apisite.chm", 2). /* or: RUN HHPopupContext (FILL-IN-1:HANDLE, "apisite.chm::/cshelp.txt", 2). */ PROCEDURE HHPopupContext : DEFINE INPUT PARAMETER phWidget AS WIDGET-HANDLE NO-UNDO. DEFINE INPUT PARAMETER pFilename AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER pContextId AS INTEGER NO-UNDO. DEFINE VARIABLE FontSpec AS CHARACTER. DEFINE VARIABLE HH_POPUP AS MEMPTR. DEFINE VARIABLE lpFileName AS MEMPTR. DEFINE VARIABLE lpFont AS MEMPTR. DEFINE VARIABLE lpPoint AS MEMPTR. DEFINE VARIABLE retval AS INTEGER NO-UNDO. SET-SIZE (lpFileName) = LENGTH(pFileName) + 1. PUT-STRING(lpFileName,1) = pFileName. /* specify a font, format "facename[, point size[, charset[ BOLD ITALIC UNDERLINE]]]" */ FontSpec = 'MS Sans Serif,10,,BOLD'. FontSpec = 'MS Sans Serif,10,,'. SET-SIZE (lpFont) = LENGTH(FontSpec) + 1. PUT-STRING(lpFont,1) = FontSpec. /* screen coordinates. There is something weird about this. */ /* I really wish I had two video adaptors to test negative screen coordinates */ SET-SIZE (lpPoint) = 8. PUT-LONG (lpPoint,1) = INTEGER(phWidget:WIDTH-PIXELS / 2). PUT-LONG (lpPoint,5) = INTEGER(phWidget:HEIGHT-PIXELS / 2). RUN ClientToScreen IN hpApi(phWidget:HWND, GET-POINTER-VALUE(lpPoint), OUTPUT retval). /* fill the HH_POPUP structure */ SET-SIZE (HH_POPUP) = 52. PUT-LONG (HH_POPUP, 1) = GET-SIZE(HH_POPUP). PUT-LONG (HH_POPUP, 5) = 0. PUT-LONG (HH_POPUP, 9) = pContextId. PUT-LONG (HH_POPUP,13) = 0. PUT-LONG (HH_POPUP,17) = GET-LONG(lpPoint,1). /* X-coordinate of center */ PUT-LONG (HH_POPUP,21) = GET-LONG(lpPoint,5). /* Y-coordinate of top */ PUT-LONG (HH_POPUP,25) = 1. /* default textcolor or RGB-VALUE(Red,Green,Blue) */ PUT-LONG (HH_POPUP,29) = -1. /* default bgcolor or RGB-VALUE(Red,Green,Blue) */ PUT-LONG (HH_POPUP,33) = -1. /* default left margin */ PUT-LONG (HH_POPUP,37) = -1. /* default top margin */ PUT-LONG (HH_POPUP,41) = -1. /* default right margin */ PUT-LONG (HH_POPUP,45) = -1. /* default bottom margin */ PUT-LONG (HH_POPUP,49) = 0. /* or get-pointer-value(lpFont). */ RUN HtmlHelpA ( phWidget:HWND, GET-POINTER-VALUE(lpFileName), {&HH_DISPLAY_TEXT_POPUP}, GET-POINTER-VALUE(HH_POPUP), OUTPUT RetVal). /* free memory */ SET-SIZE (lpFileName) = 0. SET-SIZE (lpFont) = 0. SET-SIZE (lpPoint) = 0. SET-SIZE (HH_POPUP) = 0. END PROCEDURE.
To build a CHM that contains popup strings you have to write a TXT file. The default name for this TXT file is "cshelp.txt" and it should by default be located in the CHM root. If it has a different name or a different location you have to specify it along with the name of the CHM file, as shown in the commented RUN statement above. You can also create several text files in the same CHM project in order to organize a large amount of strings. Finally the text file has to be added to the [TEXT POPUPS] section in the project file.
This file (cshelp.txt) can contain many strings and is formatted like this little example :
.topic 1 This is the first example of a popup topic. It can be called using HTMLHelp with the HH_DISPLAY_TEXT_POPUP option. see page hhpopup.html .topic 2 well this is just another example. Great isn't it?
Topic using the contexthelp-button from the title-bar explained how to create the standard windows contexthelp-button on the title bar, and how to respond to its events. This standard button is commonly used with dialog boxes but is not really suitable for (top-level) windows, especially because the contexthelp-button can only be realised when the Minimize/Maximize buttons aren't visible.
On a (top-level) window, it's probably more appropriate to create a toolbar where one of the right-most buttons invokes context-help. To implement this in Progress you can simply create a button-widget and add the following ON CHOOSE-handler to it:
{windows.i} DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. ON CHOOSE OF Btn_CTHELP IN FRAME DEFAULT-FRAME DO: RUN SendMessageA IN hpApi(c-win:HWND, 274, /* = WM_SYSCOMMAND */ 61824, /* = SC_CONTEXTHELP */ 0, OUTPUT ReturnValue). END.
This message will result in the mouse-pointer changing to the ?-symbol and will also make the MW_HELP message to be sent when the user clicks on a widget.
To catch the WM_HELP message and supply appropriate help, you will have to use the MsgBlaster control and follow the instructions on page Using the contecthelp-button from the title-bar
In Progress 9 you don't need to use a MsgBlaster anymore, you can just set c-win:context-help=true. However this is a little bit tedious, but Tom Bergman found the solution:
There's a trick you must do to make it work in
AppBuilder code. Progress will only respond to the click on a widget if the
context-help attribute of the window is set to true. Since the AppBuilder won't
let you set this attribute along with min and max buttons this presents a minor
problem.
Trying to set the attribute in the main block fails because the window has
already been realized.
The "trick" is to create an include file with the following content:
{&WINDOW-NAME}:CONTEXT-HELP = TRUE.
Add this include file as a method-library and it gets included before the window
is realized.
One of the nice features of the Progress implementation of this feature is that
you don't actually need a help file to use it. If you don't reference a help
file or context-id, progress will show you the help attribute of the widget when
you click on it using what's this help.
This topic is 32-bit only and was made in cooperation with Paul Koufalis.
This example uses the source in procedure winstyle.p, available on page WinStyle.p.
It also uses the MsgBlaster control, free available for download.
It seems common practice in Windows 95 to create a Contexthelp button in the titlebar of a dialog, rather than a 'normal' helpbutton. When the user chooses the Contexthelp button it will stay down and the mouse pointer changes into an arrow with question mark. When the user now chooses a control, Windows will send a WM_HELP message to the top-level window, releases the Contexthelp button and reloads the usual mouse-pointer.
It's up to the application to handle the WM_HELP message.
This page explains what's going on. It seems a lot of work, at least when you have to repeat all steps for every new dialog. The good news is that it must be very easy to wrap everything up into one single ActiveX control.
Added later: We actually have created this ActiveX control: cthelp.ocx and it is attached so you can download it.
The examples on this page show how to do this for a Dialog. The sources work equally well for a window but that's a bit unusual. If you do want to use this on a window please be warned that the Contexthelp-button can not be placed until the Minimize/Maximize buttons are removed from the title bar.
So on a normal window you will prefer to call context-help from a toolbar instead the title bar. In that case, see contexthelp from any button.
Let's start with adding the Help button. Create a dialog and add this source fragment to the main block:
DEFINE VARIABLE hStyle AS HANDLE NO-UNDO. RUN WinStyle.p PERSISTENT SET hStyle. RUN AddHelpButton IN hStyle (FRAME {&frame-name}:HWND). DELETE PROCEDURE hStyle. FRAME {&frame-name}:LOAD-MOUSE-POINTER("Arrow":U). RUN AddContextIDs.
Load-mouse-pointer("Arrow") is necessary for a Dialog, not for a window. I don't understand why.
The button works now and sends WM_HELP messages to the dialog. Progress normally doesn't notify you when external messages occur, so we seem to need the msgblaster control.
Drop a msgblaster on the dialog and set it up as follows:
PROCEDURE MsgBlaster.Msgblst32.MESSAGE . DEFINE INPUT PARAMETER p-MsgVal AS INTEGER NO-UNDO. DEFINE INPUT PARAMETER p-wParam AS INTEGER NO-UNDO. DEFINE INPUT PARAMETER p-lParam AS INTEGER NO-UNDO. DEFINE INPUT-OUTPUT PARAMETER p-lplRetVal AS INTEGER NO-UNDO. IF p-MsgVal = 83 /* = WM_HELP */ THEN RUN HelpContextPopup(p-lParam). END PROCEDURE. PROCEDURE initialize-controls : DEFINE VARIABLE hparent AS INTEGER NO-UNDO. DEFINE VARIABLE hc AS COM-HANDLE NO-UNDO. &IF "{&window-name}" <> "" &THEN /* if this is a window: */ hparent = GetParent({&window-name}:HWND). &ELSE /* if this is a dialog: */ hParent = FRAME {&frame-name}:HWND. &ENDIF hc = chMsgBlaster:Msgblst32. hc:MsgList(0) = 83. /* = WM_HELP */ hc:MsgPassage(0) = 1. /* or -1 or 0, didn't notice any difference */ hc:hWndTarget = hparent. RELEASE OBJECT hc. END PROCEDURE.
So now we will be notified if a WM_HELP message occurs; the message event will run the procedure HelpContextPopup. HelpContextPopup is a very general procedure e.g. not specific for one dialog, so you can safely put it in a persistent library.
PROCEDURE HelpContextPopup : /*------------------------------------------------------------------ Purpose: show context help in a popup window Parameters: p-lParam contains a pointer to a HELPINFO structure Notes: -------------------------------------------------------------------- */ DEFINE INPUT PARAMETER p-lParam AS INTEGER. DEFINE VARIABLE helpinfo AS MEMPTR. DEFINE VARIABLE ContextType AS INTEGER. DEFINE VARIABLE HWND AS INTEGER. DEFINE VARIABLE ContextID AS INTEGER. DEFINE VARIABLE ReturnValue AS INTEGER. SET-SIZE(helpinfo) = 28. SET-POINTER-VALUE(helpinfo) = p-lParam. ContextID = GET-LONG(helpinfo, 17). /* ContextID=0 will result in the standard text "No help topic is associated with this item" You might want to test that and return or replace 0 by a different (translated) ContextID */ /* Was WM_HELP called for HELPINFO_WINDOW or for HELPINFO_MENUITEM ? We don't want to support help for MENUITEM right now */ ContextType = GET-LONG(helpinfo, 5). IF ContextType<>1 /* 1=HELPINFO_WINDOW */ THEN DO: SET-SIZE(helpinfo)=0. RETURN. END. HWND = GET-LONG(helpinfo,13). RUN WinHelp{&A} IN hpApi (HWND, "myhelp.hlp", 8, /* 8 = HELP_CONTEXTPOPUP */ ContextID, OUTPUT ReturnValue). SET-SIZE(helpinfo) = 0. END PROCEDURE.
So WinHelp will be called and shows a certain ContextID from "myhelp.hlp" in a little yellow popup-window, aligned to the control where your mouse was on. Great. But what ContextID, you might wonder?
When you write a helpfile each topic will be assigned a unique integer identifier: the ContextID. The ContextHelp button feature requires that you write a lot of topics: at worst one for each widget, at best one topic for each group if widgets (like one for each (smart)frame).
The ContextID's supplied by your help authoring tool must be mapped to the widgets or widgetgroups in the program. From the main block we already called procedure AddContextIDs. Here's the implementation:
PROCEDURE AddContextIDs : /*------------------------------------------------------------------- Purpose: map widgets to ContextID's Parameters: --------------------------------------------------------------------- */ DEFINE VARIABLE retval AS INTEGER NO-UNDO. RUN SetWindowContextHelpId IN hpApi(FRAME {&frame-name}:HWND, 101, OUTPUT retval). RUN SetWindowContextHelpId IN hpApi(FRAME FRAME-A:HWND, 102, OUTPUT retval). RUN SetWindowContextHelpId IN hpApi({&window-name}:HWND, 103, OUTPUT retval). DO WITH FRAME {&frame-name}: RUN SetWindowContextHelpId IN hpAPi(button-1:HWND, 143, OUTPUT retval). RUN SetWindowContextHelpId IN hpApi(button-2:HWND, 142, OUTPUT retval). RUN SetWindowContextHelpId IN hpApi(fill-in-1:HWND, 144, OUTPUT retval). RUN SetWindowContextHelpId IN hpApi(fill-in-2:HWND, 145, OUTPUT retval). END. END PROCEDURE.
Of course these are all widgets your program probably don't have, it is just an example. The important part is that if you assign a ContextID to a window, all frames in that window will inherit that ContextID. If you assign a ContextID to a frame, all widgets in that frame will inherit that ContextID. And so on.
That makes it possible and convenient to assign ContextID's inside the sources of SmartObjects.
cthelp.zip : cthelp.ocx
Hello all. Trying to perform an HTTP POST operation using the Msxml2 DLL and a JSON payload. From all documentation, research with other developers, etc. it appears that I have most everything correct, yet I am getting ISE's (500). Web service provider hasn't been any help with diagnosing, so I have been left to try to figure it out on my own.
What DOES work:
- All GET operations, using a small variant in URL, and returning the expected JSON response.
- All operations (GET and POST) call for an authorization header, and an accept header, shown below, and working for the GET operations:
hdHTTP:setRequestHeader("Authorization", p-token).
hdHTTP:setRequestHeader("Accept","application/json; charset=UTF-8").
The only difference (according to the documentation) with the POST operation that fails is that it:
- is a POST - verified
- has a slightly different URL (for the resource) - verified
- requires a JSON payload - verified with another developer's working JSON payload
Obviously, the payload has to be specified somewhere, and I thought that it should be in the .Send method, as in:
hdHTTP:Send().
Is this the correct place to insert the payload?
.
In Windows "Control Panel" / "Regional Settings" you can choose a Locale and change several formats for that Locale. However Progress displays dates and times using its own display formats.
You can use GetDateFormatA or GetTimeFormatA to format a date or time according to Windows settings. The resulting character string can be useful for reports or display-only fields. When you call GetDateFormatA or GetTimeFormatA you will probably want it to use the current locale, although it is also possible to use any non-current locale as demonstrated in the above example window.
/* these prototypes are declared in windows.p. constants are declared in {i18n.i}. i18n.i is available in everything.zip */ PROCEDURE GetDateFormatA EXTERNAL "KERNEL32" : DEFINE INPUT PARAMETER Locale AS LONG. DEFINE INPUT PARAMETER dwFlags AS LONG. DEFINE INPUT PARAMETER lpTime AS LONG. DEFINE INPUT PARAMETER lpFormat AS LONG. DEFINE INPUT-OUTPUT PARAMETER lpDateStr AS CHARACTER. DEFINE INPUT PARAMETER cchDate AS LONG. DEFINE RETURN PARAMETER cchReturned AS LONG. END PROCEDURE. PROCEDURE GetTimeFormatA EXTERNAL "KERNEL32" : DEFINE INPUT PARAMETER Locale AS LONG. DEFINE INPUT PARAMETER dwFlags AS LONG. DEFINE INPUT PARAMETER lpTime AS LONG. DEFINE INPUT PARAMETER lpFormat AS LONG. DEFINE INPUT-OUTPUT PARAMETER lpTimeStr AS CHARACTER. DEFINE INPUT PARAMETER cchTime AS LONG. DEFINE RETURN PARAMETER cchReturned AS LONG. END PROCEDURE.
Parameter lpTime is a MEMPTR to a SYSTEMTIME structure. lpTime=0 will use the current system date/time.
Parameter lpFormat is a pointer to a format string. lpFormat=0 will use the format as specified in Control Panel. There are numerous possibilities, you best check the help file in Control Panel for examples.
Parameter lpTimeString is the returned character string. The memory for this string must be allocated in your P4GL program as usual. The size of this allocated string must be supplied in cchTime.
The demo window was based on a radio set with a couple of Language ID's:
{i18n.i} DEFINE VARIABLE RD-LANGID AS INTEGER VIEW-AS RADIO-SET VERTICAL RADIO-BUTTONS "LOCALE_USER_DEFAULT", {&LOCALE_USER_DEFAULT}, "LOCALE_SYSTEM_DEFAULT", {&LOCALE_SYSTEM_DEFAULT}, "Dutch", {&LANGID_DUTCH}, "French", {&LANGID_FRENCH}, "German", {&LANGID_GERMAN}, "Spanish", {&LANGID_SPANISH}, "English", {&LANGID_ENGLISH}, "Italian", {&LANGID_ITALIAN} SIZE 34 BY 5.24 NO-UNDO. -------------------------------------------------------------------------------- ON VALUE-CHANGED OF RD-LANGID IN FRAME DEFAULT-FRAME DO: ASSIGN rd-langid. DEFINE VARIABLE chDate AS CHARACTER NO-UNDO. DEFINE VARIABLE cchRet AS INTEGER NO-UNDO. chDate = FILL("x",50). RUN GetDateFormatA IN hpApi( RD-LANGID , 2, 0, 0, INPUT-OUTPUT chDate, LENGTH(chDate), OUTPUT cchRet ). fill-in-longdate:SCREEN-VALUE = chDate. chDate = FILL("x",50). RUN GetDateFormatA IN hpApi( RD-LANGID , 1, 0, 0, INPUT-OUTPUT chDate, LENGTH(chDate), OUTPUT cchRet ). fill-in-shortdate:SCREEN-VALUE = chDate. chDate = FILL("x",50). RUN GetTimeFormatA IN hpApi( RD-LANGID , 0, 0, 0, INPUT-OUTPUT chDate, LENGTH(chDate), OUTPUT cchRet ). fill-in-time:SCREEN-VALUE = chDate. END.
This example passes 0 for lpTime so it will always format the current system time. To format any other date or time you will have to pass a MEMPTR to an initialized SYSTEMTIME structure, for example:
{i18n.i} DEFINE VARIABLE cchRet AS INTEGER NO-UNDO. DEFINE VARIABLE lpTime AS MEMPTR NO-UNDO. SET-SIZE (lpTime) = 16. PUT-SHORT(lpTime, 1) = 1998. /* = year */ PUT-SHORT(lpTime, 3) = 6. /* = month */ PUT-SHORT(lpTime, 5) = 5. /* = day of week, ignored */ PUT-SHORT(lpTime, 7) = 26. /* = day */ PUT-SHORT(lpTime, 9) = 21. /* = hour */ PUT-SHORT(lpTime,11) = 42. /* = minute */ PUT-SHORT(lpTime,13) = 21. /* = seconds */ PUT-SHORT(lpTime,15) = 4. /* = milliseconds */ chDate = FILL("x",50). RUN GetDateFormatA IN hpApi( RD-LANGID , 0, GET-POINTER-VALUE(lpTime), 0, INPUT-OUTPUT chDate, LENGTH(chDate), OUTPUT cchRet ). SET-SIZE(lpTime) = 0.
Procedure GetLocaleInfoA can be used to retrieve all the information entered in "Control Panel / Regional Settings", both for the current locale and for all other locales. This can be useful for lots of things like displaying the actual names for days and months.
The demo window, shown in the picture, allows to choose a locale and fills a couple of selection lists showing the month names, abbreviated month names, day names and abbreviated day names. It uses the following definitions:
/* Procedure GetLocaleInfoA is declared in windows.p. Use includefile {i18n.i} to use declared constants. i18n.i is part of everything.zip */ PROCEDURE GetLocaleInfoA EXTERNAL "kernel32" : DEFINE INPUT PARAMETER Locale AS LONG. DEFINE INPUT PARAMETER dwFlags AS LONG. DEFINE INPUT-OUTPUT PARAMETER lpLCData AS CHARACTER. DEFINE INPUT PARAMETER cchData AS LONG. DEFINE RETURN PARAMETER cchReturned AS LONG. END PROCEDURE.
The radio set in the window is defined as:
{i18n.i} DEFINE VARIABLE RD-LANGID AS INTEGER VIEW-AS RADIO-SET VERTICAL RADIO-BUTTONS "LOCALE_USER_DEFAULT", {&LOCALE_USER_DEFAULT}, "LOCALE_SYSTEM_DEFAULT", {&LOCALE_SYSTEM_DEFAULT}, "Dutch", {&LANGID_DUTCH}, "French", {&LANGID_FRENCH}, "German", {&LANGID_GERMAN}, "Spanish", {&LANGID_SPANISH}, "English", {&LANGID_ENGLISH}, "Italian", {&LANGID_ITALIAN} SIZE 34 BY 5.24 NO-UNDO. -------------------------------------------------------------------------------- ON VALUE-CHANGED OF RD-LANGID IN FRAME DEFAULT-FRAME DO: ASSIGN rd-langid. DEFINE VARIABLE chName AS CHARACTER NO-UNDO. DEFINE VARIABLE i AS INTEGER NO-UNDO. DEFINE VARIABLE cchRet AS INTEGER NO-UNDO. SELECT-day:LIST-ITEMS="". DO i=0 TO 6 : chName = FILL("x",50). RUN GetLocaleInfoA IN hpApi ( RD-LANGID , {&LOCALE_SDAYNAME1} + i, INPUT-OUTPUT chName, LENGTH(chName), OUTPUT cchRet ). SELECT-day:ADD-LAST(chName). END. SELECT-abbrevday:LIST-ITEMS="". DO i=0 TO 6 : chName = FILL("x",50). RUN GetLocaleInfoA IN hpApi( RD-LANGID , {&LOCALE_SABBREVDAYNAME1} + i, INPUT-OUTPUT chName, LENGTH(chName), OUTPUT cchRet ). SELECT-abbrevday:ADD-LAST(chName). END. SELECT-month:LIST-ITEMS="". DO i=0 TO 11 : chName = FILL("x",50). RUN GetLocaleInfoA IN hpApi ( RD-LANGID , {&LOCALE_SMONTHNAME1} + i, INPUT-OUTPUT chName, LENGTH(chName), OUTPUT cchRet ). SELECT-month:ADD-LAST(chName). END. SELECT-abbrevmonth:LIST-ITEMS="". DO i=0 TO 11 : chName = FILL("x",50). RUN GetLocaleInfoA IN hpApi ( RD-LANGID , {&LOCALE_SABBREVMONTHNAME1} + i, INPUT-OUTPUT chName, LENGTH(chName), OUTPUT cchRet ). SELECT-abbrevmonth:ADD-LAST(chName). END. END.
Found in an e-mail to Peg, sent by Torben Jensby Christensen
We have been experimenting with mixing Arabic and standard Western Eropean
fields on the same screen.
For this to work as seamlessly as possible for the users we are changing
keyboard and writing direction on every field entry.
Following code is working with Progress 9.1
DEFINE VARIABLE we-keyboard AS CHARACTER INIT "00000409":U NO-UNDO. DEFINE VARIABLE arabic-keyboard AS CHARACTER INIT "00000429":U NO-UNDO. /* Used for change input keyboard behaviour (Arabic) */ PROCEDURE LoadKeyboardLayoutA EXTERNAL "user32.dll": /* Input paramets klid : keyboard layout id 00000401: Arabic (Saudi Arabia, Iraq, Egypt, Libya, Algeria, Jordan ....) 00000402: Bulgaria 00000405: Czech 00000406: Danish 00000407: German Standard 00000409: English US 0000040a: Spanish 0000040b: Finnish 0000040c: French_Standard 0000040d: Hebrew 00000410: Italian 00000413: Dutch_Standard 00000414: Norwegian 00000415: Polish 0000041a: Croatian 0000041d: Swedish 00000429: Farsi 00000807: German_Swiss 00000809: English United Kingdom 0000080a: Spanish Latin 0000080c: French_Belgian 00000813: Dutch_Belgian 00000816: Portuguese flags: specifies how the keyboard layout is to be loaded KLF_ACTIVATE &1H KLF_NOTELLSHEL KLF_REORDER KLF_REPLACELANG KLF_SUBSTITUTE_OK KLF_UNLOADPREVIOUS See keyboard layout id at: http://www.microsoft.com/globaldev/winxp/xp-lcid.asp */ DEFINE INPUT PARAMETER klid AS CHARACTER. DEFINE INPUT PARAMETER flags AS LONG. END. FUNCTION set-keyboard RETURNS LOGICAL (INPUT lng AS CHAR ). DEFINE VARIABLE klid AS CHARACTER INIT "00000409":U NO-UNDO. IF lng <> "ARABIC":U AND lng <> "NORMAL":U THEN DO: MESSAGE "set-keyboard":U SKIP "Must be called with Arabic or normal":U SKIP lng VIEW-AS ALERT-BOX ERROR BUTTONS OK. RETURN FALSE. END. IF lng = "ARABIC":U THEN DO: /* klid = "00000429":U. arabic farsi keyboard */ klid = arabic-keyboard. END. ELSE DO: /* klid = "00000409":U. English US keyboard */ klid = we-keyboard. END. ASSIGN klid = "00000409":U WHEN klid = "". RUN LoadKeyboardLayoutA IN THIS-PROCEDURE (INPUT klid,INPUT 1). /* arabic farsi keyboard */ RETURN TRUE. END FUNCTION. FUNCTION set-field-direction RETURNS LOGICAL (INPUT fillin-or-editor AS HANDLE,INPUT direction AS CHAR). DEFINE VARIABLE styles AS INTEGER NO-UNDO. IF direction <> "R2L":U AND direction <> "L2R":U THEN DO: MESSAGE "set-field-direction":U SKIP "Must be called with R2L or L2R":U SKIP direction VIEW-AS ALERT-BOX ERROR BUTTONS OK. RETURN FALSE. END. IF direction = "R2L":U THEN DO: /* styles = 4608. /* R2L arabic field */ */ styles = 29184. /* R2L + Scrollbar left Arabic field */ END. ELSE DO: styles = 512. /* L2R Normal WE field */ END. RUN SetWindowLongA IN THIS-PROCEDURE (INPUT fillin-or-editor:HWND ,INPUT -20,INPUT styles). RETURN TRUE. END FUNCTION. /* Used for fill-in and not large editor behaviour (Arabic) */ PROCEDURE SetWindowLongA EXTERNAL "user32.dll": /* Input parameters hdwnd HWND handle of fill-in or editor offs -20 for Extended windows styles newlong 4608 for arabic 29184 for arabic with vertical scrollbar 512 for WE */ DEFINE INPUT PARAMETER hdwnd AS LONG. DEFINE INPUT PARAMETER offs As LONG. DEFINE INPUT PARAMETER newlong AS LONG. END.
.
by Stuart Morris
Usefull when that process really should not be interupted. Note: It can cripple the PC
&GLOBAL-DEFINE A A &GLOBAL-DEFINE SPI_SCREENSAVERRUNNING 97 DEFINE VARIABLE retVal AS INTEGER NO-UNDO. DEFINE VARIABLE lv-dummy AS INTEGER NO-UNDO. PROCEDURE SystemParametersInfo{&A} EXTERNAL "USER32.DLL":U : DEFINE INPUT PARAMETER uAction AS LONG NO-UNDO. DEFINE INPUT PARAMETER uParam AS LONG NO-UNDO. DEFINE INPUT PARAMETER lpvParam AS LONG NO-UNDO. DEFINE INPUT PARAMETER fuWinIni AS LONG NO-UNDO. DEFINE OUTPUT PARAMETER retVal AS LONG NO-UNDO. END. /* --- Disable */ RUN SystemParametersInfo{&A} (INPUT {&SPI_SCREENSAVERRUNNING}, 1, /* True */ lv-dummy, 0, OUTPUT retVal ) NO-ERROR. /* --- Enable */ RUN SystemParametersInfo{&A} (INPUT {&SPI_SCREENSAVERRUNNING}, 0, /* False */ lv-dummy, 0, OUTPUT retVal ) NO-ERROR.
This example reads the toggle states of the three most commonly used toggle keys: Capslock, Numlock and Insert. The example also changes these three toggle states: you will see the LED's on your keyboard change.
The GetKeyboardState procedure fetches the keystates of 256 keys at once. There's also a GetKeyState procedure to fetch the state for 1 particular key, but there is no SetKeyState procedure for one key so if you need to change a keystate you will have to use SetKeyboadState and set them all.
{windows.i} DEFINE VARIABLE CapsLockToggle AS LOGICAL. DEFINE VARIABLE NumLockToggle AS LOGICAL. DEFINE VARIABLE InsertToggle AS LOGICAL. RUN GetKeyToggleState (OUTPUT CapsLockToggle, OUTPUT NumLockToggle, OUTPUT InsertToggle). /* alter all three of them, for demonstration purposes */ RUN SetKeyToggleState (NOT CapsLockToggle, NOT NumLockToggle, NOT InsertToggle). &GLOB VK_CAPITAL 20 &GLOB VK_INSERT 45 &GLOB VK_NUMLOCK 144 &GLOBAL-DEFINE VK_SCROLL 145 /* (hex 91) SCROLL LOCK key */ &GLOBAL-DEFINE VK_SHIFT 16 /* (hex 10) either SHIFT key */ &GLOBAL-DEFINE VK_CONTROL 17 /* (hex 11) either Ctrl key */ /* The following only work under Windows NT/2000/XP: */ &GLOBAL-DEFINE VK_LSHIFT 160 /* (hex A0) Left SHIFT key */ &GLOBAL-DEFINE VK_RSHIFT 161 /* (hex A1) Right SHIFT key */ &GLOBAL-DEFINE VK_LCONTROL 162 /* (hex A2) Left CTRL key */ &GLOBAL-DEFINE VK_RCONTROL 163 /* (hex A3) Right CTRL key */ PROCEDURE GetKeyToggleState : DEFINE OUTPUT PARAMETER CapsLockToggle AS LOGICAL. DEFINE OUTPUT PARAMETER NumLockToggle AS LOGICAL. DEFINE OUTPUT PARAMETER InsertToggle AS LOGICAL. DEFINE VARIABLE KBState AS MEMPTR. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(KBState) = 256. /* Get the current state of the keyboard */ RUN GetKeyboardState(GET-POINTER-VALUE(KBState), OUTPUT ReturnValue). CapsLockToggle = GET-BYTE(KBState, 1 + {&VK_CAPITAL}) MOD 2 = 1. NumLockToggle = GET-BYTE(KBState, 1 + {&VK_NUMLOCK}) MOD 2 = 1. InsertToggle = GET-BYTE(KBState, 1 + {&VK_INSERT}) MOD 2 = 1. SET-SIZE(KBState) = 0. END PROCEDURE. /* ======================================================= */ PROCEDURE SetKeyToggleState : DEFINE INPUT PARAMETER CapsLockToggle AS LOGICAL. DEFINE INPUT PARAMETER NumLockToggle AS LOGICAL. DEFINE INPUT PARAMETER InsertLockToggle AS LOGICAL. DEFINE VARIABLE KBState AS MEMPTR. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(KBState) = 256. /* Get the current state of the keyboard */ RUN GetKeyboardState(GET-POINTER-VALUE(KBState), OUTPUT ReturnValue). PUT-BYTE(KBState,1 + {&VK_CAPITAL}) = IF CapsLockToggle THEN 1 ELSE 0. PUT-BYTE(KBState,1 + {&VK_NUMLOCK}) = IF NumLockToggle THEN 1 ELSE 0. PUT-BYTE(KBState,1 + {&VK_INSERT}) = IF InsertLockToggle THEN 1 ELSE 0. RUN SetKeyboardState(INPUT GET-POINTER-VALUE(KBState), OUTPUT ReturnValue). SET-SIZE(KBState) = 0. END PROCEDURE. PROCEDURE GetKeyboardState EXTERNAL {&USER}: DEFINE INPUT PARAMETER KBState AS LONG. /* memptr */ DEFINE RETURN PARAMETER RetVal AS LONG. /* bool */ END PROCEDURE. PROCEDURE SetKeyboardState EXTERNAL {&USER} : DEFINE INPUT PARAMETER KBState AS LONG. /* memptr */ DEFINE RETURN PARAMETER RetVal AS LONG. /* bool */ END PROCEDURE.
GetKeyboardState fetches an array of 256 keystates, each keystate is one byte in size.
VK_CAPITAL (better known as Capslock) has index 20 in that array. Arrays in C are zero based while Progress starts counting at one, so the put-byte and get-byte statements are called with (1 + {&VK_CAPITAL}).
A key is toggled ON when the low-order bit of its keystate is set to 1. The example tests this by checking if the value is odd or even.
A key is pressed when the high-order bit is set to 1. This may be important to know, but not in this example.
Although the code does change the toggle state for the Insert-button you will not notice it; for some reason the Progress fill-in and editor widgets will keep behaving as if Insert is toggled on.
The Statusbar ActiveX from Microsoft Common Controls V5 has panels that automatically represent the toggle states. However, these panels don't respond to the code.
Based on an idea by Paul Koufalis.
.
The Progress fill-in widget has a :blank attribute that you can use when creating a password field. But most Windows applications use asterisks instead of blanks.
It's fairly simple to have asterisks (or any other password-character) in Progress too:
{windows.i} DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. RUN SendMessage{&A} IN hpApi (fill-in_password:HWND, {&EM_SETPASSWORDCHAR}, ASC("*"), 0, OUTPUT ReturnValue).
The third parameter is the new passwordchar. When 0, you disable the password feature.
You may have trouble reading fill-in_password:screen-value so instead you have to ASSIGN the fill-in and test the associated field or variable.
There is a small security risc: if you use debugtools like Spy++ you will see the contents of the fill-in in readable format, e.g. with no asterisks. This means it is fairly simple for a hacker to create a program that logs your passwords. This is not only true for this particular fill-in, but for every password field in MS-Windows. The normal :BLANK attribute however can not be spied: it only shows blanks.
The 'name' field (in a login dialog) can be initialized with the name by which the user is known in Windows.
You can try to find this name in the registry but you better let Windows do it for you, using the GetUserName function.
{windows.i} DEFINE VARIABLE NAME AS CHARACTER NO-UNDO. RUN WinUserName(OUTPUT NAME). fill-in_name:SCREEN-VALUE = NAME. PROCEDURE WinUserName : DEFINE OUTPUT PARAMETER NAME AS CHARACTER. DEFINE VARIABLE nr AS INTEGER NO-UNDO INITIAL 100. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. NAME = FILL(" ", nr). RUN GetUserName{&A} IN hpApi (INPUT-OUTPUT NAME, INPUT-OUTPUT nr, OUTPUT ReturnValue). END PROCEDURE.
The 'fill' statement is important because it allocates memory. Windows hardly ever allocates memory for you.
There is one important drawback: the function GetUserName does not exist in 16-bit windows. If you are using a Progress version prior to 8.2 on Windows 95 you can still use this function but you will have to thunk it.
.
Sometimes you may need to force focus to a certain widget.
There must be several solutions but I found this one to be fairly reliable: make Windows think the user clicked on the widget using his left mouse button. 'Click' is a combination of 'buttondown' and 'buttonup' so it involves two messages, but I assume you might be allowed to skip one of them.
So if you want to apply focus to FILL-IN-1 you might say:
run MouseClick(FILL-IN-1:HANDLE).
{windows.i} PROCEDURE MouseClick : DEFINE INPUT PARAMETER hWidget AS HANDLE NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. RUN PostMessage{&A} IN hpApi(hWidget:HWND, {&WM_LBUTTONDOWN}, {&MK_LBUTTON}, 0, OUTPUT ReturnValue). RUN PostMessage{&A} IN hpApi(hWidget:HWND, {&WM_LBUTTONUP}, 0, 0, OUTPUT ReturnValue). END PROCEDURE.
You might have noticed the use of PostMessage instead of SendMessage like in apply the right mouse button. I have no idea why SendMessage doesn't work for the left mouse button.
Or: programatic dropping a popup-menu
Sometimes you will want to "apply" a mouse click to a widget, for example to give it focus (with the standard left button) or to force a popup-menu to pop up (with the alternate button).
In this example we have an image widget; the user can perform several actions on that image widget (like loading a new bmp-file or whatever). Those actions are listed in a popup-menu and now you want that popup-menu to appear whenever the user clicks the widget, even when he uses the left mouse button. Unfortunately the P4GL statement 'APPLY "MOUSE-MENU-CLICK" doesn't work but you can fool Progress by sending the appropriate mouse messages.
ON "MOUSE-SELECT-CLICK" OF IMAGE-1 DO: RUN CenterMouseCursor(SELF:HANDLE). RUN Apply-mouse-menu-click(SELF:HANDLE). END.
The first statement, CenterMouseCursor, moves the mouse to the middle of the widget so this will be the exact location where the upper-left corner of the popup-menu will appear. This is by itself not very important and you may want to skip or change it. The real work is done by the Apply-mouse-menu-click.
Here is the api stuff:
{windows.i} PROCEDURE Apply-mouse-menu-click : /*------------------------------------------------------------------------------ Purpose: Programatic click the right mouse button on a widget Parameters: Widget-handle on which you want to click ------------------------------------------------------------------------------*/ DEFINE INPUT PARAMETER p-wh AS WIDGET-HANDLE NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. RUN SendMessage{&A} IN hpApi (INPUT p-wh:HWND, INPUT {&WM_RBUTTONDOWN}, INPUT {&MK_RBUTTON}, INPUT 0, OUTPUT ReturnValue). RUN SendMessage{&A} IN hpApi (INPUT p-wh:HWND, INPUT {&WM_RBUTTONUP}, INPUT 0, INPUT 0, OUTPUT ReturnValue). END PROCEDURE. PROCEDURE CenterMouseCursor : /*------------------------------------------------------------------------------ Purpose: Move the mouse cursor to the middle of a widget Parameters: the widget-handle ------------------------------------------------------------------------------*/ DEFINE INPUT PARAMETER p-wh AS WIDGET-HANDLE NO-UNDO. DEFINE VARIABLE lppoint AS MEMPTR NO-UNDO. /* POINT FAR* */ DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lppoint)= 2 * {&INTSIZE}. PUT-{&INT}(lppoint,1 + 0 * {&INTSIZE})=INTEGER(p-wh:WIDTH-PIXELS / 2). PUT-{&INT}(lppoint,1 + 1 * {&INTSIZE})=INTEGER(p-wh:HEIGHT-PIXELS / 2). RUN ClientToScreen IN hpApi (INPUT p-wh:HWND, INPUT GET-POINTER-VALUE(lppoint), OUTPUT ReturnValue). RUN SetCursorPos IN hpApi (INPUT GET-{&INT}(lppoint,1 + 0 * {&INTSIZE}), INPUT GET-{&INT}(lppoint,1 + 1 * {&INTSIZE}), OUTPUT ReturnValue). SET-SIZE(lppoint)= 0. END PROCEDURE.
Procedure SetCursorPos works with absolute screen coordinates where the upper left corner of your screen has coordinates (0,0).
But the widget coordinates p-wh:X and p-wh:Y are relative to p-wh:parent.
The conversion is done by function ClientToScreen (there is also a ScreenToClient function) which has two params: a handle and a memorypointer to a POINT structure.
Function ClientToScreen changes the contents of the POINT structure.
Based on a procedure by Rod Gaither
based on an example by Chris Braddock
There is no real MOUSE-OVER event but this example shows one possible way to achieve the same effect.
The general idea is: use the PSTimer.OCX. Read the mouse coordinates on every tick of the timer using API-function GetCursorPos. The resulting coordinates are screen-coordinates. Then use API-function ScreenToClient to convert the screen-coordinates to client-coordinates, where "client" is the window you are interested in. Finally, test if those client coordinates are inside the client rectangle.
Attached example (in mousexy.zip) continuously shows the screen-coordinates and the client-coordinates. It also colors the frame green when the mouse moves over the frame, and colors the frame grey when the mouse moves away from the frame.
The example is based on these two API functions: GetCursorPos and ScreenToClient.
.
It can be attractive to play an animation, for example while the user has to wait while your application communicates with the AppServer.
The source in this example uses some of the MCI series of API commands, making it possible to use this source on a window or on a dialog, and making it possible to play AVI's with or without soundtrack!
The complete example source (aviwin.w) is attached. It is a UIB-generated window (not SmartWindow) so the source is a bit too large to show on this page.
The source relies on the availablity of windows.i and windows.p, timestamp Sep 8, 1997 or later.
The window contains two buttons: button-open and button-stop.
Button-open is used to open (and play) an AVI file, button-close calls the procedure to stop (and close) the AVI file. The AVI file repeats infinitely and has a low priority in order to perform other Progress tasks, like FOR EACH processing in the meantime.
Normally you won't offer your users these buttons, they are here for demonstration purposes only.
The window also contains a frame (frame fdisplay): this will be the target window where the AVI output will be shown. It does not have to be a frame: you might also play the AVI on a button...
The AVI file is not opened in shared mode, so if you forget to Close the AVI-player you won't be able to open the AVI file again!
This makes it extremely important to run the 'close' procedure when the AVI is not needed anymore: you must never close the window without closing the AVI first! This is implemented by 'run PlayerClose' after the mainblock's 'WAIT-FOR' loop. If you use this code in a SmartObject you should run PlayerClose in local-destroy or somewhere around there.
anim.zip : download source example
Function PlaySoundA plays a waveform sound. This function replaces the obsolete function sndPlaySoundA.
&GLOB SND_ASYNC 1 &GLOB SND_NODEFAULT 2 &GLOB SND_LOOP 8 &GLOB SND_PURGE 64 &GLOB SND_APPLICATION 128 &GLOB SND_ALIAS 65536 &GLOB SND_FILENAME 131072 &GLOB SND_RESOURCE 262148 PROCEDURE PlaySoundA EXTERNAL "winmm.dll" PERSISTENT : DEFINE INPUT PARAMETER pszSound AS LONG. DEFINE INPUT PARAMETER hmod AS LONG. DEFINE INPUT PARAMETER fdwSound AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.
Notice the 'persistent' option: if the procedure is not declared as persistent it will not be possible to use the SND_ASYNC flag. Also notice that pszSound is declared as LONG. This is not convenient in most cases when you need to pass a character string, but has to be long to support {&SND_PURGE}.
In the first example, the first parameter is interpreted as a filename. De default system sound ('ting') will be played if the specified filename can not be found, because the flag SND_NODEFAULT is not specified.
DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE szSound AS MEMPTR NO-UNDO. DEFINE VARIABLE wavfile AS CHARACTER NO-UNDO. wavfile = "c:\windows\media\logoff.wav". SET-SIZE(szSound) = LENGTH(wavfile, "raw":U) + 1. PUT-STRING(szSound,1) = wavfile. RUN PlaySoundA (GET-POINTER-VALUE(szSound), 0, {&SND_FILENAME}, OUTPUT ReturnValue). SET-SIZE(szSound) = 0.
The next example plays the system sound associated in Registry with eventname "SystemExit".
This sound and others are found in registry key "HKCU\AppEvents\Schemes\Apps\.Default".
DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE szSound AS MEMPTR NO-UNDO. DEFINE VARIABLE eventname AS CHARACTER NO-UNDO. eventname = "SystemExit". SET-SIZE(szSound) = LENGTH(eventname, "raw":U) + 1. PUT-STRING(szSound,1) = eventname. RUN PlaySoundA (GET-POINTER-VALUE(szSound), 0, {&SND_ALIAS} + {&SND_NODEFAULT}, OUTPUT ReturnValue). SET-SIZE(szSound) = 0.
The next example plays an application-specific sound event.
These can be registered in key "HKCU\AppEvents\Schemes\Apps\prowin32".
DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE szSound AS MEMPTR NO-UNDO. DEFINE VARIABLE eventname AS CHARACTER NO-UNDO. eventname = "CUSTOMER_DELETED". SET-SIZE(szSound) = LENGTH(eventname, "raw":U) + 1. PUT-STRING(szSound,1) = eventname. RUN PlaySoundA (GET-POINTER-VALUE(szSound), 0, {&SND_APPLICATION} + {&SND_NODEFAULT}, OUTPUT ReturnValue). SET-SIZE(szSound) = 0.
It is also possible to link WAV resources into an executable or DLL. Suppose the DLL is identified by handle hSounds and contains a sound resource named "LOGIN_REJECTED" :
DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE szSound AS MEMPTR NO-UNDO. DEFINE VARIABLE eventname AS CHARACTER NO-UNDO. eventname = "LOGIN_REJECTED". SET-SIZE(szSound) = LENGTH(eventname, "raw":U) + 1. PUT-STRING(szSound,1) = eventname. RUN PlaySoundA (GET-POINTER-VALUE(szSound), hSounds, {&SND_RESOURCE} + {&SND_NODEFAULT}, OUTPUT ReturnValue). SET-SIZE(szSound) = 0.
The last example is submitted by Nenad Orlovic [norlovic@zg.tel.hr].
Sounds should always be played asynchronous, especially when it is a long sound clip, so the program can continue while the sound is playing. This is done by adding the SND_ASYNC flag. This is only possible when the PlaySound procedure is declared as persistent: otherwise Progress would free the winmm.dll library immediately which would cause the sound to stop.
The example also uses SND_LOOP to repeat the sound. SND_LOOP can not be used without SND_ASYNC.
DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE szSound AS MEMPTR NO-UNDO. DEFINE VARIABLE wavfile AS CHARACTER NO-UNDO. wavfile = "c:\windows\media\logoff.wav". SET-SIZE(szSound) = LENGTH(wavfile, "raw":U) + 1. PUT-STRING(szSound,1) = wavfile. RUN PlaySoundA (GET-POINTER-VALUE(szSound), 0, {&SND_FILENAME} + {&SND_ASYNC} + {&SND_LOOP} + {&SND_NODEFAULT}, OUTPUT ReturnValue). SET-SIZE(szSound) = 0. MESSAGE "Press OK to stop the music" VIEW-AS ALERT-BOX. RUN PlaySoundA (0, 0, {&SND_PURGE}, OUTPUT ReturnValue). MESSAGE "The music stopped" VIEW-AS ALERT-BOX.
.
by Todd G. Nist
This program utilizes the standard DoFileDownload API.
On the occasions when you simply need to download one file, this routine will display the IE file download dialog. The nice thing is that the dialog takes care of all aspects of the interface, including the progressbar, animation and statistics. This code can easily be integrated into any application requiring this type of functionality.
The program has been tested under Windows 2000 Server/Professional.
----
API-procedures used in this example are listed here to be included in the search index:
PROCEDURE DoFileDownload EXTERNAL 'shdocvw.dll':U: PROCEDURE WideCharToMultiByte EXTERNAL "KERNEL32.dll": PROCEDURE MultiByteToWideChar EXTERNAL "KERNEL32.dll":
ftpdownload.w.zip : demo
by Todd G. Nist
Program source is available for download: winftp.w. Modified 20 July 2000.
This program demonstrates common FTP functionality: connect to an FTP server, list directory contents, change the current directory and of course: get, put and delete files.
See also FileDownload.
The code functions as follows:
* By default, it will connect to ftp.progress.com when you press the Connect FTP button.
* Once on the site, if you double click a directory in the directory selection list, it will open that directory and display the contents.
* By selecting the appropriate button, put, get, delete, you can achieve the desired request. These are all based on having permissions to the web site.
The function FtpFindFirstFile can only be invoked once within a given FTP Session. Therefore, when a double click occurs on the directory selection list, the current FTP session is closed, and then a new one established and the function FTPDirList invoked. This function is similar to the FindFirstFile and FindNextFile win32 API calls.
No username or password are used when connecting to the FTP server. This means the default username ("anonymous") and default password (something like "IE40USER@") will be used. This will probably not give you enough permissions to put or delete items. You may have to pass more specific username and password parameters in function InternetConnectionA.
The program has been tested under NT 4.0 with service pack 3 and IE4 or IE5, and also under Windows 98 with IE5.
----
API-procedures used in this example are listed here to be included in the search index:
PROCEDURE InternetConnectA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE InternetGetLastResponseInfoA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE InternetOpenUrlA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE InternetOpenA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE InternetReadFile EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE InternetCloseHandle EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE FtpFindFirstFileA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE InternetFindNextFileA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE FtpGetCurrentDirectoryA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE FtpSetCurrentDirectoryA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE FtpOpenFileA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE FtpPutFileA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE FtpGetFileA EXTERNAL "wininet.dll" PERSISTENT: PROCEDURE FtpDeleteFileA EXTERNAL "wininet.dll" PERSISTENT:
winftp.w.zip : demo program
by Maurits van Rijnen
Every network adapter has a unique id: the MAC address. The MAC address may be used to identify a computer, at least for a while until its network adapter is replaced or until someone decides to flash a new MAC address into the adapter. Also, be carefull if the computer has more than one network adapter.
To read the MAC address you should read the ARP cache, but that's very complicated. Let's forget about the ARP cache.
Maurits found a neat "workaround" based on UuidCreate:
UuidCreate creates a new universally unique identifier, which can be used for creating new ActiveX controls and so on. Because it has to be universally unique it is based on a MAC address and probably some datetime-value (and perhaps some other hardware metrics). Maurits recognized how to read the MAC address from the uuid :
PROCEDURE UuidCreate EXTERNAL "rpcrt4.dll":U : DEFINE INPUT-OUTPUT PARAMETER opi-guid AS CHARACTER NO-UNDO. END PROCEDURE. PROCEDURE UuidCreateSequential EXTERNAL "rpcrt4.dll":U : DEFINE INPUT-OUTPUT PARAMETER opi-guid AS CHARACTER NO-UNDO. END PROCEDURE. FUNCTION inttohex RETURNS CHARACTER (INPUT i AS INTEGER): /* only for 0..255 integer values */ DEFINE VARIABLE cHex AS CHARACTER NO-UNDO INIT '0123456789ABCDEF':U. DEFINE VARIABLE j1 AS INTEGER NO-UNDO. DEFINE VARIABLE j2 AS INTEGER NO-UNDO. j1 = TRUNCATE(i / 16, 0) . j2 = i - (j1 * 16). RETURN SUBSTR(cHex, j1 + 1, 1) + SUBSTR(cHex, j2 + 1, 1). END. FUNCTION GetMacAddress RETURNS CHAR: DEFINE VARIABLE X AS CHARACTER NO-UNDO. DEFINE VARIABLE i AS INTEGER NO-UNDO. DEFINE VARIABLE j AS INTEGER NO-UNDO. DEFINE VARIABLE r AS CHARACTER NO-UNDO. X = FILL(' ':U, 16). IF RunningWindows2000() THEN RUN UuidCreateSequential (INPUT-OUTPUT X). ELSE RUN UuidCreate (INPUT-OUTPUT X). DO i = 11 TO 16: r = r + ' ':U + inttohex(ASC(SUBSTR(X,i,1))). END. RETURN SUBSTR(R,2). END. DISPLAY GetMAcAddress() FORMAT "X(20)":U.
Procedure UuidCreate() in Windows 2000 returns a uuid that can not be traced back to the MAC address. Procedure UuidCreateSequential is provided for backward compatibility: it behaves like UuidCreate() in Windows 95/98/NT4.
There are several techniques for Inter Process Communication (IPC) like DDE, pipes, atoms, sockets and mailslots. Each technique has its own characteristics; this topic concentrates on Mailslots.
Mailslots offer an easy way for a process to broadcast a message to several other processes at once. A Mailslot is a pseudo-file created by one particular process, known as the Mailslot server. The Maislot server can read messages from the mailslot.
Other processes, known as Mailslot clients, can write messages to mailslots owned by Mailslot servers. A Mailslot client is not allowed to read from another process' mailslot.
Remember: a mailslot is a pseudo-file and lives in memory only for the lifetime of the process, or shorter. Don't confuse it with email where messages are persistently stored in an "inbox folder" or file.
For the sake of simplicity it's easiest to think of processes as 'computers'. It is possible to set up mailsots between processes on the same computer but this has some limitations.
In it's simplest form, a server can receive messages from one or many clients but the clients can not receive anything.
In this case, Sue has set up a mailslot on her local machine. She named this mailslot
\\.\mailslot\myapp\finance
(The "dot" means "local machine". "myapp" and "finance" are path and name to be picked by your application). This path and name have to be known by other processes. That won't be a problem if those other processes are running instances of the same application!
Bob and Pete are clients; they can write messages to Sue's mailslot by addressing
\\Sue\mailslot\myapp\finance
Where "Sue" is a machine name. More realisticly, they would address to
\\domainname\mailslot\myapp\finance
or even to
\\*\mailslot\myapp\finance
where the asterisk stands for the primary domain. In these last two cases, the messages will also be picked up by anyone else in the domain who defined the mailslot like Sue did. Hence you have a broadcasting mechanism.
Although Sue is the mailslot server for her own local mailslot, there is no reason why she shouldn't be able to send messages to other processes who also have a local mailslot named ".\mailslot\myapp\finance". Sue can be a server and a client at the same time.
Likewise, there is no reason why Bob and Pete should't be able to serve a mailslot each, also using this same mailslot name.
A message sent by either Bob, Pete or Sue (or anyone else) will now be recieved by all others.
Only one process on a computer can serve a mailslot with a particular name. In other words: this broadcasting mechanism will not work between multiple instances of an application running on the same computer.
The maximum size of a message is 64K if it is not broadcast. A broadcasted message can be no longer than 400 byte.
Mailslots use datagrams. There is no way of knowing if a broadcasted message will actually be received by everyone.
If you need to broadcast across a WAN you may consider setting up sockets on an "IP Multicast" enabled network. See http:www.ipmulticast.com.
A server creates a mailslot by calling procedure CreateMailslot(). This procedure returns a Mailslot-handle. The Mailslot-handle is somewhat compatible with a file-handle: the server can read messages from the mailslot by calling procedure ReadFile(). Procedure GetMailslotInfo() tells how many messages are in the pseudo-file and how long they are. The server calls CloseHandle() to destroy the mailslot.
A client writes to a mailslot just as if it writes to a file, except the filename must be a valid mailslot-name. In other words a client uses CreateFile(), WriteFile() and CloseHandle().
Note: CreateFile() doesn't create the file of course, it only gets a handle to the existing pseudo-file (the "share"-flags are especially important here).
Source code by Todd. G. Nist, Protech Systems Inc.
Source is attached for download.
This procedure can run either as a mailslot server or as a mailslot client. If it runs as a server, it will be able to read messages (it uses a PSTimer.ocx to scan for new messages each 500 msec). If it runs as a client it is able to send messages if there is also a server active.
I (Jurjen) had only one PC to play with, so I launched two instances of Progress on that PC, each running a copy of w-mailslot.w as shown on the pic.
First you have to start a mailslot server: run w-mailslot.w and make sure the toggle-box is checked. Click on the editor-widget: the ON ENTRY trigger of the editor will create the mailslot. From now on the PSTimer will check for new messages and will show them in the editor.
After you have created a server you can start one or more clients: run w-mailslot.w again and make sure the toggle-box is NOT checked. Change the mailslot name if the server is not running on the same PC. Click on the editor widget: the ON-ENTRY trigger will now open the mailslot for writing and the PSTimer.ocx will be closed.
Type a message, press the "Write" button and watch how the server receives it.
This program will not act as a server and a client at the same time, so let's look at it as if it were two separate programs.
These three parts do the core functionality: CreateMailslot, ReadMailslot and CloseHandle.
RUN CreateMailSlot{&A} (INPUT cMailslotName:SCREEN-VALUE, INPUT 0, /* Maximum message length */ INPUT 0, /* Read timeout */ INPUT 0, /* security attributes */ OUTPUT hMailSlot). /* handle to mailslot or INVALID_HANDLE_VALUE */ -------------------------------------------------------------------------------- FUNCTION ReadMailSlot RETURNS CHARACTER ( /* parameter-definitions */ ) : DEFINE VARIABLE cTempStr AS CHARACTER NO-UNDO. DEFINE VARIABLE iBytesRead AS INTEGER NO-UNDO. DEFINE VARIABLE iResultCode AS INTEGER NO-UNDO. /* allocate some space */ cTempStr = FILL(' ', 512). RUN ReadFile (INPUT hMailslot, OUTPUT cTempStr, INPUT 512, OUTPUT iBytesRead, INPUT 0, OUTPUT iResultCode). RETURN TRIM(cTempStr) + (IF TRIM(cTempStr) = '' THEN '' ELSE CHR(10)). END FUNCTION. -------------------------------------------------------------------------------- RUN CloseHandle(hMailslot, OUTPUT iResultCode).
Function ReadFile() reads one message even if there are more messages pending. The size (512) may not be enough. I think you should call function GetMailslotInfo() first: this returns the number of pending messages (so you can read all of them) and the size of the next message (so you can allocate the proper size).
The most important parts are now: CreateFile, WriteMailSlot and CloseHandle.
RUN CreateFile{&A}( INPUT cMailslotName:SCREEN-VALUE, {&GENERIC_WRITE}, {&FILE_SHARE_READ}, 0, {&OPEN_EXISTING}, {&FILE_ATTRIBUTE_NORMAL}, 0, OUTPUT hMailslot). -------------------------------------------------------------------------------- FUNCTION WriteMailSlot RETURNS CHARACTER ( /* parameter-definitions */ ) : DEFINE VARIABLE iBytesWritten AS INTEGER NO-UNDO. DEFINE VARIABLE iResultCode AS INTEGER NO-UNDO. /* Write to the mailslot */ cMsg = "\\":U + cComputerName + " - ":U + cMsg:SCREEN-VALUE IN FRAME {&frame-name}. RUN WriteFile(INPUT hMailslot, INPUT cMsg, INPUT LENGTH(cMsg) + 1, OUTPUT iBytesWritten, INPUT 0, OUTPUT iResultCode). IF iResultCode = 0 THEN DO: MESSAGE "Error on WriteFile. " "Terminating client." VIEW-AS ALERT-BOX. APPLY "window-close" TO {&window-name}. END. ELSE cMsg:SCREEN-VALUE = "". RETURN "". /* Function return value. */ END FUNCTION. -------------------------------------------------------------------------------- RUN CloseHandle(hMailslot, OUTPUT iResultCode).
mailslot.zip : demo
by Marian EDU
Marian made a function to check if the host is alive or not. He wrote:
"if you want to get funky you can use gethostbyname to resolve the address, and use more options on IcmpSendEcho. but I don't see no utility in this. you probably want to check if a specific service is running on that machine. so it's more easy to use sockets and attempt to connect to the specific service."
/****************************************************************************** Program: ping.p Written By: Marian EDU Written On: September 2002 Description: Used to do ping or traceroute to one specific host address. Host name is not supported, works only with IP address. Parameters: Input - IP address - ping && traceroute options - show result message flag Output - host available flag Note: Options: You can specify ping && traceroute options as a comma delimited string. ex: '-t,-n 10,-i 20,-l 32,-w 300' will send maximum 10 echo requests with 32 bytes of data for each host on trace route with the 300 milliseconds time-out and the maximum TTL is 20 and traceroute is enabled. -t enable traceroute -n number of request to send -i time to live TTL -l send packet size -w time-out in milliseconds to wait for reply Examples: ping.p('66.218.71.86', '-t,-w 300,-n 10,-l 320,-i 20', TRUE, OUTPUT lAvail) Will do a traceroute to yahoo servers using a 320 bytes data packet, with a maximum hops number of 20 (TTL), for each host in trace route will send a maximum 10 echo request until will get an answer using 300 milliseconds time-out. Cause the show result message flag is true will display the traceroute result at the end. --------------------- Revision History ------------------ Date: Author Change Description 23/09/02 M EDU Initial Release 24/09/02 M EDU Traceroute implemented, more options available ******************************************************************************/ DEFINE INPUT PARAMETER pcHostAddr AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER pcOptions AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER plShowResults AS LOGICAL NO-UNDO. DEFINE OUTPUT PARAMETER plAlive AS LOGICAL NO-UNDO. DEFINE VARIABLE iNoRetry AS INTEGER NO-UNDO. DEFINE VARIABLE iPacketSize AS INTEGER NO-UNDO. DEFINE VARIABLE iTimeOut AS INTEGER NO-UNDO. DEFINE VARIABLE iMaxHops AS INTEGER NO-UNDO. DEFINE VARIABLE lEnableTrace AS LOGICAL NO-UNDO. DEFINE VARIABLE ReqData AS MEMPTR NO-UNDO. DEFINE VARIABLE ReplyBuf AS MEMPTR NO-UNDO. DEFINE VARIABLE PIP_OPTION_INFORMATION AS MEMPTR NO-UNDO. DEFINE VARIABLE HopAddr AS MEMPTR NO-UNDO. DEFINE VARIABLE iCount AS INTEGER NO-UNDO EXTENT 3. DEFINE VARIABLE iRes AS INTEGER NO-UNDO. DEFINE VARIABLE iIcmpHdl AS INTEGER NO-UNDO. DEFINE VARIABLE iDstAddr AS INTEGER NO-UNDO. DEFINE VARIABLE cHostAddr AS CHARACTER NO-UNDO. DEFINE VARIABLE cEntry AS CHARACTER NO-UNDO. DEFINE VARIABLE cMessage AS CHARACTER NO-UNDO. /* API definitions */ /* Microsoft has their own proprietary API for ping and tracert implemented in ICMP.DLL. The functions in ICMP.DLL are not considered part of the Win32 API and might not be supported in future releases. */ PROCEDURE IcmpCreateFile EXTERNAL 'ICMP.DLL': DEFINE RETURN PARAMETER phIcmp AS LONG. END PROCEDURE. PROCEDURE IcmpCloseHandle EXTERNAL 'ICMP.DLL': DEFINE INPUT PARAMETER phIcmp AS LONG. END PROCEDURE. PROCEDURE IcmpSendEcho EXTERNAL 'ICMP.DLL': DEFINE INPUT PARAMETER phIcmp AS LONG. DEFINE INPUT PARAMETER DstAddr AS LONG. DEFINE INPUT PARAMETER ReqData AS LONG. DEFINE INPUT PARAMETER ReqSize AS LONG. DEFINE INPUT PARAMETER ReqOptions AS LONG. DEFINE INPUT PARAMETER ReplyBuf AS LONG. DEFINE INPUT PARAMETER ReplySize AS LONG. DEFINE INPUT PARAMETER Timeout AS LONG. DEFINE RETURN PARAMETER ReplyCount AS LONG. END PROCEDURE. PROCEDURE inet_addr EXTERNAL 'WS2_32.DLL': DEFINE INPUT PARAMETER HostName AS CHARACTER. DEFINE RETURN PARAMETER HostAddr AS LONG. END PROCEDURE. PROCEDURE inet_ntoa EXTERNAL 'WS2_32.DLL': DEFINE INPUT PARAMETER HostAddr AS LONG. DEFINE RETURN PARAMETER HostName AS MEMPTR. END PROCEDURE. /* default options */ ASSIGN lEnableTrace = FALSE iPacketSize = 32 iTimeOut = 5000 iNoRetry = 4 iMaxHops = 64 NO-ERROR. /* parse options parameter */ DO iCount[1] = 1 TO NUM-ENTRIES(pcOptions): cEntry = ENTRY(iCount[1],pcOptions). CASE ENTRY(1,cEntry,' ':U): WHEN '-t':U THEN lEnableTrace = TRUE. WHEN '-w' THEN iTimeOut = INTEGER(ENTRY(NUM-ENTRIES(cEntry,' ':U),cEntry,' ':U)) NO-ERROR. WHEN '-n' THEN iNoRetry = INTEGER(ENTRY(NUM-ENTRIES(cEntry,' ':U),cEntry,' ':U)) NO-ERROR. WHEN '-l' THEN iPacketSize = INTEGER(ENTRY(NUM-ENTRIES(cEntry,' ':U),cEntry,' ':U)) NO-ERROR. WHEN '-i' THEN iMaxHops = INTEGER(ENTRY(NUM-ENTRIES(cEntry,' ':U),cEntry,' ':U)) NO-ERROR. END CASE. END. SET-SIZE(ReqData) = iPacketSize + 1. DO iCount[1] = 1 TO iPacketSize: PUT-STRING(ReqData,iCount[1]) = CHR(32 + iCount[2]). iCount[2] = iCount[2] + 1. IF iCount[2] >= 94 THEN iCount[2] = 0. END. SET-SIZE(ReplyBuf) = GET-SIZE(ReqData) + 28 + 1. SET-SIZE(PIP_OPTION_INFORMATION) = 4 + 1. SET-SIZE(HopAddr) = 16. RUN inet_addr(pcHostAddr, OUTPUT iDstAddr) NO-ERROR. RUN IcmpCreateFile(OUTPUT iIcmpHdl) NO-ERROR. /* if valid host IP address suplied */ IF iDstAddr NE -1 AND iIcmpHdl NE -1 THEN DO: /* traceroute - increment TTL and send a new echo request */ IF lEnableTrace THEN DO iCount[1] = 1 TO iMaxHops: PUT-LONG(PIP_OPTION_INFORMATION,1) = iCount[1]. DO iCount[2] = 1 TO iNoRetry: RUN IcmpSendEcho(iIcmpHdl, iDstAddr, GET-POINTER-VALUE(ReqData), GET-SIZE(ReqData), GET-POINTER-VALUE(PIP_OPTION_INFORMATION), GET-POINTER-VALUE(ReplyBuf), GET-SIZE(ReplyBuf), iTimeOut, OUTPUT iRes). IF iRes > 0 THEN LEAVE. END. RUN inet_ntoa(GET-LONG(ReplyBuf,1), OUTPUT HopAddr). /* format the treceroute result message */ IF plShowResults THEN cMessage = cMessage + SUBSTITUTE('Reply from &1~t time=&2ms~t TTL=&3~n', GET-STRING(HopAddr,1), STRING(GET-LONG(ReplyBuf,9)), STRING(iCount[1])). IF iDstAddr = GET-LONG(ReplyBuf,1) THEN DO: plAlive = TRUE. LEAVE. END. END. /* ping - send a number of requests using the given TTL, time-out, packet size */ ELSE DO iCount[1] = 1 TO iNoRetry: PUT-LONG(PIP_OPTION_INFORMATION,1) = iMaxHops. RUN IcmpSendEcho(iIcmpHdl, iDstAddr, GET-POINTER-VALUE(ReqData), GET-SIZE(ReqData), GET-POINTER-VALUE(PIP_OPTION_INFORMATION), GET-POINTER-VALUE(ReplyBuf), GET-SIZE(ReplyBuf), iTimeOut, OUTPUT iRes). IF iRes = 0 THEN NEXT. RUN inet_ntoa(GET-LONG(ReplyBuf,1), OUTPUT HopAddr). /* format the ping result message */ IF plShowResults THEN cMessage = cMessage + SUBSTITUTE('Reply from &1~t bytes=&2~t~ttime=&3ms~t TTL=&4~n', GET-STRING(HopAddr,1), STRING(GET-LONG(ReplyBuf,13)), STRING(GET-LONG(ReplyBuf,9)), STRING(GET-LONG(ReplyBuf,21))). IF iRes > 0 THEN plAlive = TRUE. END. END. RUN IcmpCloseHandle(iIcmpHdl). SET-SIZE(HopAddr) = 0. SET-SIZE(PIP_OPTION_INFORMATION) = 0. SET-SIZE(ReqData) = 0. SET-SIZE(ReplyBuf) = 0. IF cMessage NE '':U THEN MESSAGE cMessage VIEW-AS ALERT-BOX.
by Bill Prew
The following procedure returns the ip adress and the host name for the local computer.
&SCOPED-DEFINE WSADESCRIPTION_LEN 256 &SCOPED-DEFINE WSASYS_STATUS_LEN 128 &SCOPED-DEFINE WSADATA_VERSION_LOW 1 /* WORD(2) */ &SCOPED-DEFINE WSADATA_VERSION_HIGH 3 /* WORD(2) */ &SCOPED-DEFINE WSADATA_DESCRIPTION 5 /* CHAR(WSADESCRIPTION_LEN + 1) */ &SCOPED-DEFINE WSADATA_SYSTEM_STATUS 262 /* CHAR(WSASYS_STATUS_LEN + 1) */ &SCOPED-DEFINE WSADATA_MAX_SOCKETS 391 /* SHORT(4) */ &SCOPED-DEFINE WSADATA_MAX_UDP 395 /* SHORT(4) */ &SCOPED-DEFINE WSADATA_VENDOR_INFO 399 /* CHAR*(4) */ &SCOPED-DEFINE WSADATA_LENGTH 403 &SCOPED-DEFINE HOSTENT_NAME 1 /* CHAR*(4) */ &SCOPED-DEFINE HOSTENT_ALIASES 5 /* CHAR**(4) */ &SCOPED-DEFINE HOSTENT_ADDR_TYPE 9 /* SHORT(2) */ &SCOPED-DEFINE HOSTENT_ADDR_LENGTH 11 /* SHORT(2) */ &SCOPED-DEFINE HOSTENT_ADDR_LIST 13 /* CHAR**(4) */ &SCOPED-DEFINE HOSTENT_LENGTH 16 PROCEDURE i-GetTcpInfo: /*------------------------------------------------------------------------ Procedure : i-GetTcpInfo Description : Return the windows TCP host name and address of this PC. Parms : - Host name. (OUTPUT, CHARACTER) - Host address. (OUTPUT, CHARACTER): Sample usage: RUN i-GetTcpInfo (OUTPUT w-TcpName, OUTPUT w-TcpAddr). Notes : - ------------------------------------------------------------------------*/ DEFINE OUTPUT PARAMETER p-TcpName AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER p-TcpAddr AS CHARACTER NO-UNDO. DEFINE VARIABLE w-TcpName AS CHARACTER NO-UNDO. DEFINE VARIABLE w-Length AS INTEGER NO-UNDO. DEFINE VARIABLE w-Return AS INTEGER NO-UNDO. DEFINE VARIABLE ptr-WsaData AS MEMPTR NO-UNDO. DEFINE VARIABLE w-Hostent AS INTEGER NO-UNDO. DEFINE VARIABLE ptr-Hostent AS MEMPTR NO-UNDO. DEFINE VARIABLE ptr-AddrString AS MEMPTR NO-UNDO. DEFINE VARIABLE ptr-AddrList AS MEMPTR NO-UNDO. DEFINE VARIABLE ptr-ListEntry AS MEMPTR NO-UNDO. DEFINE VARIABLE w-TcpLong AS INTEGER NO-UNDO. /* Initialize return values */ ASSIGN p-TcpName = ? p-TcpAddr = ? . /* Allocate work structure for WSADATA */ SET-SIZE(ptr-WsaData) = {&WSADATA_LENGTH}. /* Ask Win32 for winsock usage */ RUN WSAStartup (INPUT 257, /* requested version 1.1 */ INPUT GET-POINTER-VALUE(ptr-WsaData), OUTPUT w-Return). /* Release allocated memory */ SET-SIZE(ptr-WsaData) = 0. /* Check for errors */ IF w-Return NE 0 THEN DO: MESSAGE "Error accessing WINSOCK support." VIEW-AS ALERT-BOX. RETURN. END. /* Set up variables */ ASSIGN w-Length = 100 w-TcpName = FILL(" ", w-Length) . /* Call Win32 routine to get host name */ RUN gethostname (OUTPUT w-TcpName, INPUT w-Length, OUTPUT w-Return). /* Check for errors */ IF w-Return NE 0 THEN DO: MESSAGE "Error getting tcp name." VIEW-AS ALERT-BOX. RUN WSACleanup (OUTPUT w-Return). RETURN. END. /* Pass back gathered info */ /* remember: the string is null-terminated so there is a CHR(0) inside w-TcpName. We have to trim it: */ p-TcpName = ENTRY(1,w-TcpName,CHR(0)). /* Call Win32 routine to get host address */ RUN gethostbyname (INPUT w-TcpName, OUTPUT w-Hostent). /* Check for errors */ IF w-Hostent EQ 0 THEN DO: MESSAGE "Error resolving host name." VIEW-AS ALERT-BOX. RUN WSACleanup (OUTPUT w-Return). RETURN. END. /* Set pointer to HostEnt data structure */ SET-POINTER-VALUE(ptr-Hostent) = w-Hostent. /* "Chase" pointers to get to first address list entry */ SET-POINTER-VALUE(ptr-AddrList) = GET-LONG(ptr-Hostent, {&HOSTENT_ADDR_LIST}). SET-POINTER-VALUE(ptr-ListEntry) = GET-LONG(ptr-AddrList, 1). w-TcpLong = GET-LONG(ptr-ListEntry, 1). RUN inet_ntoa (INPUT w-TcpLong, OUTPUT ptr-AddrString). /* Pass back gathered info */ p-TcpAddr = GET-STRING(ptr-AddrString, 1). /* Terminate winsock usage */ RUN WSACleanup (OUTPUT w-Return). END PROCEDURE.
Definitions used in this procedure, not listed in windows.p :
PROCEDURE gethostname EXTERNAL "wsock32.dll" : DEFINE OUTPUT PARAMETER p-Hostname AS CHARACTER. DEFINE INPUT PARAMETER p-Length AS LONG. DEFINE RETURN PARAMETER p-Return AS LONG. END PROCEDURE. PROCEDURE gethostbyname EXTERNAL "wsock32.dll" : DEFINE INPUT PARAMETER p-Name AS CHARACTER. DEFINE RETURN PARAMETER p-Hostent AS LONG. END PROCEDURE. PROCEDURE inet_ntoa EXTERNAL "wsock32.dll" : DEFINE INPUT PARAMETER p-AddrStruct AS LONG. DEFINE RETURN PARAMETER p-AddrString AS MEMPTR. END PROCEDURE. PROCEDURE WSAStartup EXTERNAL "wsock32.dll" : DEFINE INPUT PARAMETER p-VersionReq AS SHORT. DEFINE INPUT PARAMETER ptr-WsaData AS LONG. DEFINE RETURN PARAMETER p-Return AS LONG. END PROCEDURE. PROCEDURE WSACleanup EXTERNAL "wsock32": DEFINE RETURN PARAMETER p-Return AS LONG. END PROCEDURE.
On Windows 98 and perhaps other versions of Windows you may prefer to use "ws32_2.dll" instead "wsock32.dll".
June 22, 1999
Joern Winther found and solved the following bug:
p-TcpName = w-TcpName.
should be
p-TcpName = ENTRY(1,w-TcpName,CHR(0)).
to get rid of the terminating null.
(This is only a very brief overview of printing)
Printing is basically the same as drawing on screen: both are handled through the GDI (Graphic Device Interface) who translates your high level drawing commands to low level instructions according to the appropriate device drivers. This driver can be a printer driver, a display driver, fax driver or whatever.
Before you can start to draw anything you must first define on which device you will draw, ie you must get a handle to a device context (HDC). When you want to draw on screen you use the function hdc=GetDC(hwnd) and when you want to draw on a printer you use the function hdc=CreateDC(0,printername,0,lpInitData).
Nearly every GDI function has hdc as its first parameter so it is a very important handle. When you are finished drawing you must use ReleaseDC(hdc) because Windows can only create a limited amount of context handles simultaniously.
In **pseudo-code** your program will have a structure like this:
hdc=CreateDC(...) StartDoc(hdc). StartPage(hdc). TextOut(hdc,10,10,'this is page 1',14). EndPage(hdc). StartPage(hdc). TextOut(hdc,10,10,'this is page 2',14). EndPage(hdc). EndDoc(hdc). DeleteDC(hdc).
Everything between StartDoc and EndDoc is one spooler job, this is important for a multipage document because it will prevent other jobs from printing pages in between.
As I said, this is just pseudo-code. Click here for a working code example.
You will need to know the size of the paper, in pixels. This and other important data can be obtained from GetDeviceCaps and DeviceCapabilities.
GDI has a huge amount of drawing functions. The most commonly used are:
* MoveTo(hdc,x,y) : moves the dc cursor to coordinate x,y
* LineTo(hdc,x,y) : draws a line from current position to x,y
* TextOut(hdc,x,y,text,lenght(text))
There are also several functions to manipulate text placement, see SetTextAlign. To write text in columns it is convenient to use the ExtTextOut function because it also has a rectangle parameter: the text will be clipped to the rectangle when needed and you can also easily ensure that text is centered or right-aligned to the rectangle.
You will need objects like pens, fonts, brushes, bitmaps. For example when you want to draw a red dotted line you will have to create a red dotted pen first using hRedpen=CreatePen(PS_DOT,1,RGB(255,0,0)).
But when you want to create a black solid line, you don't have to create one because it is already 'in stock'. GDI has several stock objects.
After you created one or more objects you must select one of them to make it the current object using SelectObject(hRedpen). Every created object must be manually destroyed too or else you will run out of GDI resources, using DeleteObject(hRedpen). An object can not be destroyed when it is currently selected so you may have to select a stock object first.
By Nenad Orlovic, norlovic@zg.tel.hr
Suppose you have a file that contains printer control codes, and now you want to send this file to a printer. The file may have been created by a report engine when the port was set to "FILE:".
The following procedure copies the file to a printer.
This example will send file to printer even if in printer properties port is FILE:
This is because variable OutFileName = "", but it can be changed to a filename or perhaps even to a UNC-name for a printer.
See the notes near the bottom of this page.
DEFINE INPUT PARAMETER PrinterName AS CHARACTER NO-UNDO. /* As set in Printer properties */ DEFINE INPUT PARAMETER FILENAME AS CHARACTER NO-UNDO. DEFINE VARIABLE X AS INTEGER NO-UNDO. DEFINE VARIABLE hPrinter AS INTEGER NO-UNDO. DEFINE VARIABLE hFile AS INTEGER NO-UNDO. DEFINE VARIABLE pBuf AS MEMPTR NO-UNDO. DEFINE VARIABLE FileSize AS INTEGER NO-UNDO. DEFINE VARIABLE iSize AS INTEGER NO-UNDO. DEFINE VARIABLE xSize AS INTEGER NO-UNDO. DEFINE VARIABLE pFileName AS MEMPTR NO-UNDO. DEFINE VARIABLE OutFileName AS CHARACTER NO-UNDO. DEFINE VARIABLE pOutFileName AS MEMPTR NO-UNDO. DEFINE VARIABLE DataType AS CHARACTER NO-UNDO. DEFINE VARIABLE pDataType AS MEMPTR NO-UNDO. DEFINE VARIABLE pDocInfo AS MEMPTR NO-UNDO. RUN OpenPrinterA (PrinterName,OUTPUT hPrinter,0, OUTPUT X). IF X = 0 THEN MESSAGE "Error opening printer: " PrinterName VIEW-AS ALERT-BOX. ELSE DO: RUN CreateFileA (FILENAME , -2147483648,0,0,3,128,0,OUTPUT hFile). /* -2147483648 = $80000000 */ IF hFile = -1 THEN MESSAGE "Error opening file: " FILENAME VIEW-AS ALERT-BOX. ELSE DO: RUN GetFileSize (hFile,0,OUTPUT FileSize). IF FileSize = -1 THEN MESSAGE "Wrong file size" VIEW-AS ALERT-BOX. ELSE DO: SET-SIZE(pBuf) = FileSize. RUN ReadFile(hFile,pBuf,FileSize,OUTPUT iSize,0, OUTPUT X). IF X = 0 THEN MESSAGE "Error reading file: " FILENAME VIEW-AS ALERT-BOX. ELSE DO: IF iSize = 0 THEN MESSAGE "Attempt to read beyond end of file:" FILENAME VIEW-AS ALERT-BOX. ELSE DO: OutFileName = "". DataType = "RAW". SET-SIZE(pDocInfo) = 12. SET-SIZE(pFileName) = LENGTH(FILENAME) + 1. PUT-STRING(pFileName,1) = FILENAME. SET-SIZE(pOutFileName) = LENGTH(OutFileName) + 1. PUT-STRING(pOutFileName,1) = OutFileName. SET-SIZE(pDataType) = LENGTH(DataType) + 1. PUT-STRING(pDataType,1) = DataType. PUT-LONG(pDocInfo,1) = GET-POINTER-VALUE(pFileName). PUT-LONG(pDocInfo,5) = GET-POINTER-VALUE(pOutFileName). PUT-LONG(pDocInfo,9) = GET-POINTER-VALUE(pDataType). RUN StartDocPrinterA (hPrinter,1,pDocInfo,OUTPUT X). IF X = 0 THEN DO: RUN GetLastError(OUTPUT X). MESSAGE "Error : " X VIEW-AS ALERT-BOX. END. RUN WritePrinter(hPrinter,pBuf,iSize,OUTPUT xSize,OUTPUT X). IF X = 0 THEN DO: RUN GetLastError(OUTPUT X). MESSAGE "Error writing to printer: " PrinterName iSize xsize X VIEW-AS ALERT-BOX. END. RUN EndDocPrinter(hPrinter,OUTPUT X). END. END. END. RUN CloseHandle(hFile,OUTPUT X). IF X = 0 THEN MESSAGE "Error closing file: " FILENAME. END. RUN ClosePrinter(hPrinter,OUTPUT X). IF X = 0 THEN MESSAGE "Error closing printer: " PrinterName VIEW-AS ALERT-BOX. END. SET-SIZE(pBuf) = 0. SET-SIZE(pDocInfo) = 0. SET-SIZE(pFileName) = 0. SET-SIZE(pOutFileName) = 0. SET-SIZE(pDataType) = 0. /******************/ /* DLL Procedures */ /******************/ PROCEDURE GetLastError EXTERNAL "kernel32.dll" : DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE StartDocPrinterA EXTERNAL "winspool.drv" : DEFINE INPUT PARAMETER hPrinter AS LONG. DEFINE INPUT PARAMETER Level AS LONG. DEFINE INPUT PARAMETER pDocInfo AS MEMPTR. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE EndDocPrinter EXTERNAL "winspool.drv" : DEFINE INPUT PARAMETER hPrinter AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE CreateFileA EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER lpFileName AS CHARACTER. DEFINE INPUT PARAMETER dwDesiredAccess AS LONG. DEFINE INPUT PARAMETER dwShareMode AS LONG. DEFINE INPUT PARAMETER lpSecurityAttributes AS LONG. DEFINE INPUT PARAMETER dwCreationDistribution AS LONG. DEFINE INPUT PARAMETER dwFlagsAndAttributes AS LONG. DEFINE INPUT PARAMETER hTemplateFile AS LONG. DEFINE RETURN PARAMETER hFile AS LONG. END PROCEDURE. PROCEDURE ReadFile EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hFile AS LONG. DEFINE INPUT PARAMETER lpBuffer AS MEMPTR. DEFINE INPUT PARAMETER nNumberOfBytesToRead AS LONG. DEFINE OUTPUT PARAMETER lpNumberOfBytesRead AS LONG. DEFINE INPUT PARAMETER lpOverlapped AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE WritePrinter EXTERNAL "winspool.drv" : DEFINE INPUT PARAMETER hPrinter AS LONG. DEFINE INPUT PARAMETER pBuf AS MEMPTR. DEFINE INPUT PARAMETER cbBuf AS LONG. DEFINE OUTPUT PARAMETER lpdwWritten AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE OpenPrinterA EXTERNAL "winspool.drv" : DEFINE INPUT PARAMETER pPrinterName AS CHARACTER. DEFINE OUTPUT PARAMETER phPrinter AS LONG. DEFINE INPUT PARAMETER pDefault AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE ClosePrinter EXTERNAL "winspool.drv" : DEFINE INPUT PARAMETER hPrinter AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE GetFileSize EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hFile AS LONG. DEFINE INPUT PARAMETER lpFileSizeHigh AS LONG. DEFINE RETURN PARAMETER FileSize AS LONG. END PROCEDURE. PROCEDURE CloseHandle EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hObject AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE.
Note from Valther Bernardi:
The procedure fails if the printer is a local printer and not a network printer.
For local printers the variable OutFileName need to be initialized to the value of the device associated to printer (i.e. LPT1:). For retrieving the port name associated to the printer you can use EnumPrinters
Note from Brian Maher:
In Progress 4GL you could simply output to the printer and then do a PUT CONTROL NULL which switches us into "windows passthru printing" mode.
EnumPrinter retrieves a list of available printers, that is: local installed printers and also printers made available by the network.
Actually EnumPrinters retrieves an array of PRINTER_INFO_X records (where X stands for level: will be explained later). Each PRINTER_INFO_X record contains information about a single printer, in this example we will only show the Printername (like "HP Laserjet 4L") and Portname (like "LPT1:").
Let's take a look at the source first and explain later.
{windows.i} DEFINE VARIABLE pPrinterEnum AS MEMPTR NO-UNDO. DEFINE VARIABLE pcbNeeded AS INTEGER NO-UNDO. DEFINE VARIABLE pcReturned AS INTEGER NO-UNDO. DEFINE VARIABLE RetValue AS INTEGER NO-UNDO. DEFINE VARIABLE pPrinterInfo AS MEMPTR NO-UNDO. DEFINE VARIABLE StructSize AS INTEGER INITIAL 84. DEFINE VARIABLE i AS INTEGER NO-UNDO. DEFINE VARIABLE lpPrinterName AS MEMPTR NO-UNDO. DEFINE VARIABLE lpPortName AS MEMPTR NO-UNDO. /* The first call to EnumPrinters is only to get the required memory size */ SET-SIZE(pPrinterEnum) = 30. /* A default bobo value */ RUN EnumPrinters{&A} IN hpApi(2, /* = PRINTER_ENUM_LOCAL */ "", 2, GET-POINTER-VALUE(pPrinterEnum), GET-SIZE(pPrinterEnum), OUTPUT pcbNeeded, OUTPUT pcReturned, OUTPUT RetValue). /* RetValue will now be FALSE (=error) because we did not supply enough memory. But at least we know now how much memory was required (pcbNeeded) and also how many printers were found (pcReturned) */ /* no printers installed, then return (rare) */ IF pcbNeeded=0 THEN DO: MESSAGE "No printers found". RUN DeAlloc. RETURN. END. /* Reset the size of pPrinterEnum to the correct size */ SET-SIZE(pPrinterEnum) = 0. SET-SIZE(pPrinterEnum) = pcbNeeded. /* The second call actually fills the pPrinterEnum structure */ RUN EnumPrinters{&A} IN hpApi(2, /* = PRINTER_ENUM_LOCAL */ "", 2, GET-POINTER-VALUE (pPrinterEnum), GET-SIZE(pPrinterEnum), OUTPUT pcbNeeded, OUTPUT pcReturned, OUTPUT RetValue). /* pPrinterEnum holds a couple of PRINTER_INFO_2 records. the number of records is pcReturned. the number of bytes copied to pPrinterEnum is pcbNeeded. size of one PRINTER_INFO_2 record is 84 bytes. */ DO i=0 TO pcReturned - 1 : SET-POINTER-VALUE(pPrinterInfo) = GET-POINTER-VALUE(pPrinterEnum) + (i * StructSize). /* the second LONG field in the PRINTER_INFO_2 structure is a pointer to a string holding the printer name */ SET-POINTER-VALUE(lpPrinterName) = GET-LONG(pPrinterInfo, 5). /* the 4th LONG field in the PRINTER_INFO_2 structure is a pointer to a string holding the port name */ SET-POINTER-VALUE(lpPortName) = GET-LONG(pPrinterInfo,13). MESSAGE "printername=" GET-STRING(lpPrinterName,1) SKIP "portname=" GET-STRING(lpPortName,1) VIEW-AS ALERT-BOX. END. /* Clean Up */ RUN DeAlloc. PROCEDURE DeAlloc: SET-SIZE(pPrinterEnum) = 0. END.
The first parameter (flags) in EnumPrinters is set to PRINTER_ENUM_LOCAL but Windows 95 will also enumerate network printers because these are also handled by the local print provider. Windows NT will strictly enumerate locally installed printers. When you want to enumerate network printers on NT you will have to use other parameters.
The third parameter (level) is set to 2 indicating we expect records of type PRINTER_INFO_2. Windows 95 supports only levels 1,2,5 and NT supports only levels 1,2,4.
structsize=84 is only true for Level=2.
Brad Long enhanced the previous example to support all other PRINTER_INFO types and make it independent of Windows version. The sourcefile is attached.
This piece of source is also interesting for those who struggle with memptr variables (aren't we all?).
pPrinterEnum is a pointer to an array of PRINTER_INFO_2 records. The pointer to the first record is the same as the pointer to the array, the pointer to the second record is (first) + structsize, and so on.
A PRINTER_INFO_2 record does not contain strings but pointers to strings, this indirection is solved by the lpPortName and lpPrinterName MEMPTR variables. Of course you should always use statements like SET-SIZE(memptrvar)=0 for each MEMPTR variable near the end of your procedure, but in this particular example you should not do this for pPrinterInfo because the value of this variable points to memory inside the scope of pPrinterEnum. So if you deallocate pPrinterEnum you automatically deallocate pPrinterInfo. But what about the two string-pointers lpPortName and lpPrinterName, you wonder? Well, these also point to locations within pPrinterEnum as we will see:
There are only 2 printers installed on my PC, so I would expect that pcbNeeded=(2 * 84)=168 but the function returned 948. Almost 700 byte too much: this is where the strings are stored that are pointed to by the several string-pointers in the PRINTER_INFO_2 records.
That is smart thinking by Microsoft: you now have local copies of the strings so the pointers don't have to point to protected memory, and it also solves the important question which process should be responsible for deallocating the string space. The strings are inside Your block of memory so You deallocate them. And it is all done automatically by simple deallocating pPrinterEnum.
Here's a small map to illustrate it all:
wingetprinters.p.zip : example
The attached code returns an array of Bin IDs and Bin Names for the printer/port specified in pcPrinterName and pcPrinterPort.
In 16-bit windows you can find the printer name in WIN.INI, section "windows" key "device".
For example:
[windows] device=HP LaserJet 4L,HPPCL5MS,LPT1:
To read 'WIN.INI' you use the procedure GetProfileString.
To read any other ini-file you use the procedure GetPrivateProfileString.
Example:
{windows.i} DEFINE VARIABLE printername AS CHARACTER NO-UNDO. DEFINE VARIABLE cchRet AS INTEGER NO-UNDO. printername = FILL(" ",100). /* = allocate memory, don't forget! */ RUN GetProfileString{&A} IN hpApi ("windows", "device", "-unknown-,", OUTPUT printername, LENGTH(printername), OUTPUT cchRet). /* split name from driver and port. Note that the default "-unknown-," must have at leat one comma or the ENTRY function may raise an error */ printername = ENTRY(1,printername). /* use the result */ IF printername="-unknown-" THEN MESSAGE "something is wrong with your WIN.INI" VIEW-AS ALERT-BOX. ELSE MESSAGE "your default printer is: " printername VIEW-AS ALERT-BOX.
The above example was used for an in-depth explanation of parameter types on page using a MEMPTR parameter for a CHAR. It also explains why the FILL statement is important.
Of course you can also use the 4GL procedure GET-KEY-VALUE to read this information, as shown in topic "printing: using StartDoc"
Here is the easiest way:
Printername = SESSION:PRINTER-NAME.
Although INI files are obsolete in 32-bits Windows versions, you should still use GetProfileString to find the default printer. The function will not actually read the INI file but will read from Registry.
If you know the key where the printer name is stored in Registry, you could use the following code example but you should not do that. It is always a bad idea to use hardcoded paths because Microsoft does not commit to support those paths forever. For example: Windows 95 and NT 4.0 don't use the same key. Function GetProfileString however is guaranteed to return the right information on all 32-bit Windows versions.
but it's still interesting to have a registry example, so here it goes:
DEFINE VARIABLE Printername AS CHARACTER. LOAD "System" BASE-KEY "HKEY_CURRENT_CONFIG". USE "System". GET-KEY-VALUE SECTION "CurrentControlSet\Control\Print\Printers" KEY "default" VALUE Printername. UNLOAD "System". /* code example by Joseph Richardson */
The example program printfile.p demonstrates how to print an ascii-file using GDI functions.
The program prints a header and a footer on every page using a proportional font. The page header contains the filename (right aligned). The page footer contains the date (left aligned), time (centered) and page number (right aligned).
The contents of the ascii-file is printed in a non-proportional font (Courier New).
The procedure printfile.p has currently only one input parameter: the filename of the ascii-file to print.
The definition section contains some variables who could also have been used as input parameters:
/* ================================================================= file : printfile.p purpose : print a text file usage : RUN printfile.p ("printfile.p"). tested : only on Windows 98 with local printer (HP Laserjet 4) ================================================================= */ DEFINE INPUT PARAMETER p-Filename AS CHARACTER NO-UNDO. /* these variables could also be useful as input parameters: */ DEFINE VARIABLE p-Landscape AS LOGICAL NO-UNDO INITIAL NO. /* Orientation is Landscape or Portrait */ DEFINE VARIABLE p-Silent AS LOGICAL NO-UNDO INITIAL YES. /* show no message boxes (on error) */ DEFINE VARIABLE p-Devicename AS CHARACTER NO-UNDO. /* if "" then default printer */ DEFINE VARIABLE p-docname AS CHARACTER NO-UNDO. /* if "" then p-Filename */ DEFINE VARIABLE p-Leftmargin AS DECIMAL NO-UNDO INITIAL 1.0. /* left margin (inches) from paper edge */ DEFINE VARIABLE p-fontsize AS INTEGER NO-UNDO INITIAL 9. /* pointsize for bodyfont */
API-procedures used in printfile.zip: CreateDC, DeleteDC, DocumentProperties, GetProfileString, StartDoc, EndDoc, StartPage, EndPage, TextOut, SetTextAlign, GetDeviceCaps, MoveToEx, LineTo, SelectObject, DeleteObject, GetTextMetrics, CreateFont, MulDiv.
printfile.zip : example
The attached example shows how to use the WebBrowser control (in shdocvw.dll) to print an HTML page. The example does __not__ show how to change headers/footers at run-time.
printurl.zip : a dialog with WebBrowser control
by Nickolay Borshukov
Download attached file: preview.zip
The example programs in preview.zip are an extended version of printfile.zip (see topic how to print an ASCII file).
The most noticeable extensions are:
* long lines are wrapped to the next line.
* the print layout can be viewed in a Preview window. This window is made with P4GL and Windows API-calls
API-procedures used in preview.zip (in addition to the ones in printfile.p) : CreateCompatibleDC, CreateCompatibleBitmap, SetBkMode, SetTextColor, PatBlt, StretchBlt, GetTextExtentPoint32
preview.zip : example by Nickolay Borshukov
Notice how this source uses the GET-KEY-VALUE function to retrieve information about the default printer. There are a couple of other methods to get the same information, see topic GetDefaultPrinter.
Besides using the default printer you can also use a different printer; those other printers can be picked using the EnumPrinters function.
code example by Roland the Pijper, converted to 32-bit by Jurjen
{windows.i} DEFINE VARIABLE windir AS CHARACTER. DEFINE VARIABLE pdocname AS MEMPTR. DEFINE VARIABLE poutbuf AS MEMPTR. DEFINE VARIABLE lpdocinfo AS MEMPTR. DEFINE VARIABLE pfilename AS MEMPTR. DEFINE VARIABLE outsize AS INTEGER NO-UNDO. DEFINE VARIABLE printerhDC AS INTEGER NO-UNDO. DEFINE VARIABLE apistatus AS INTEGER NO-UNDO. DEFINE VARIABLE docname AS CHARACTER NO-UNDO. DEFINE VARIABLE devicebuf AS CHARACTER NO-UNDO. DEFINE VARIABLE driverbuf AS CHARACTER NO-UNDO. DEFINE VARIABLE initbuf AS CHARACTER NO-UNDO. DEFINE VARIABLE outbuf AS CHARACTER NO-UNDO. DEFINE VARIABLE windowsdir AS CHARACTER NO-UNDO. DEFINE VARIABLE winini AS CHARACTER NO-UNDO. DEFINE VARIABLE printerdev AS CHARACTER NO-UNDO. DEFINE VARIABLE FILENAME AS CHARACTER NO-UNDO. DEFINE VARIABLE tekst AS CHARACTER NO-UNDO. /* Get printer info from WIN.INI */ windir = FILL("x", 260). RUN GetWindowsDirectoryA IN hpApi(OUTPUT windir, LENGTH(windir), OUTPUT outsize). ASSIGN windowsdir = SUBSTRING(windir,1,outsize) winini = windowsdir + "\WIN.INI". LOAD winini. USE winini. GET-KEY-VALUE SECTION "windows" KEY "device" VALUE printerdev. UNLOAD winini. IF printerdev = "" THEN DO: RUN Err ("Could not locate printer device!"). RETURN. END. ASSIGN devicebuf = ENTRY(1,printerdev). driverbuf = ENTRY(2,printerdev). outbuf = ENTRY(3,printerdev). docname = "test". /* Setup pointers to the strings needed in the lpdocinfo STRUCT */ SET-SIZE(pdocname) = LENGTH(docname) + 1. PUT-STRING(pdocname,1) = docname. SET-SIZE(poutbuf) = LENGTH(outbuf) + 1. PUT-STRING(poutbuf,1) = outbuf. /* Load up the lpdocinfo STRUCT */ SET-SIZE(lpdocinfo) = 4 /* INTEGER cbSize */ + 4 /* pointer lpszDocName */ + 4. /* pointer lpszOutput */ PUT-LONG(lpdocinfo,1) = 12. /* size of the STRUCT */ PUT-LONG(lpdocinfo,5) = GET-POINTER-VALUE(pdocname). /* pointer to CHARACTER */ PUT-LONG(lpdocinfo,9) = GET-POINTER-VALUE(poutbuf). /* pointer to CHARACTER */ tekst = "bla bla". /* Print it! */ RUN adecomm/_setcurs.p ("WAIT"). RUN CreateDCA (driverbuf, devicebuf, outbuf, 0, OUTPUT printerhDC). RUN StartDocA IN hpApi(INPUT printerhDC, INPUT GET-POINTER-VALUE(lpdocinfo), OUTPUT apistatus). /* is printjob id */ RUN StartPage IN hpApi(INPUT printerhDC, OUTPUT apistatus). RUN TextOutA IN hpApi(printerhDC, 800, 450, tekst, LENGTH(tekst), OUTPUT apistatus). IF apistatus=0 /* 0=FALSE */ THEN MESSAGE "There was an error during TextOut " VIEW-AS ALERT-BOX ERROR. RUN EndPage IN hpApi(printerhDC, OUTPUT apistatus). RUN EndDoc IN hpApi(printerhDC, OUTPUT apistatus). RUN DeleteDC(printerhDC, OUTPUT apistatus). /* Clean Up */ SET-SIZE(lpdocinfo) = 0. SET-SIZE(pdocname) = 0. SET-SIZE(poutbuf) = 0. RUN adecomm/_setcurs.p ("").
by Jurjen, improved by Ian Keene
/* ============================================================== file : printscreen.p by : Jurjen Dijkstra (Modified by Ian Keene Oct 2003) dd : 05/16/1999 purpose : draw a window to the default printer. usage : RUN printscreen.p ({&WINDOW-NAME}:HWND, YES). parms : hWindow (integer) HWND of the 'widget' to be drawn GetParent (logical) -Use YES if HWND is a Progress window widget so printscreen.p will draw the border/titlebar. -Use NO for all other widgets. ============================================================== */ DEFINE INPUT PARAMETER hWindow AS INTEGER NO-UNDO. /* HWND */ DEFINE INPUT PARAMETER GetParent AS LOGICAL NO-UNDO. /* hWindow=GetParent(hWindow)? */ RUN MakeDocument. /* API definitions used in this proc have been moved to printscreen.i */ {printscreen.i} PROCEDURE MakeDocument : DEFINE VARIABLE hDC AS INTEGER NO-UNDO. DEFINE VARIABLE lpDocInfo AS MEMPTR NO-UNDO. DEFINE VARIABLE lpDocName AS MEMPTR NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE prt-buf AS CHARACTER NO-UNDO. DEFINE VARIABLE prt-out AS CHARACTER NO-UNDO. DEFINE VARIABLE err-check AS INTEGER NO-UNDO. DEFINE VARIABLE drbuf AS CHARACTER NO-UNDO. DEFINE VARIABLE out-buf AS CHARACTER NO-UNDO. DEFINE VARIABLE prt-hwnd AS INTEGER NO-UNDO. ASSIGN prt-out = FILL(" ",127). /* ALLOCATE MEMORY */ RUN GetProfileStringA (INPUT "WINDOWS", INPUT "DEVICE", INPUT "-unknown-,", OUTPUT prt-out, INPUT LENGTH(prt-out), OUTPUT err-check). IF prt-out = "-unknown-," THEN DO: MESSAGE "Aborted, Cannot Determine Default Printer." SKIP VIEW-AS ALERT-BOX INFORMATION. RETURN. END. ASSIGN prt-buf = ENTRY(1,prt-out) drbuf = ENTRY(2,prt-out) out-buf = ENTRY(3,prt-out). /* OPEN THE PRINTER */ RUN OpenPrinterA (INPUT prt-buf, OUTPUT prt-hwnd, INPUT 0). RUN CreateDCA ( "WINSPOOL", prt-buf, 0, 0, OUTPUT hDC) . IF hDC = 0 THEN DO: MESSAGE "Error during CreateDCA in PrintScreen procedure" view-as ALERT-BOX INFORMATION. RETURN. END. SET-SIZE (lpDocName) = LENGTH("PrintScreen") + 1. PUT-STRING(lpDocName,1) = "PrintScreen". SET-SIZE (lpDocInfo) = 12. PUT-LONG (lpDocInfo,1) = 12. PUT-LONG (lpDocInfo,5) = GET-POINTER-VALUE(lpDocName). PUT-LONG (lpDocInfo,9) = 0. RUN StartDocA ( hDC, GET-POINTER-VALUE(lpDocInfo), OUTPUT ReturnValue). IF ReturnValue < 1 THEN MESSAGE "Error during StartDoc in PrintScreen procedure" view-as ALERT-BOX INFORMATION. ELSE DO: RUN StartPage (hDC, OUTPUT ReturnValue). IF ReturnValue < 1 THEN MESSAGE "Error during StartPage in PrintScreen procedure" view-as ALERT-BOX INFORMATION. ELSE DO: RUN PrintWindow (hDC, hWindow). RUN EndPage (hDC, OUTPUT ReturnValue). END. RUN EndDoc (hDC, OUTPUT ReturnValue). END. /* cleanup */ RUN DeleteDC (hDC, OUTPUT ReturnValue). SET-SIZE(lpDocInfo) = 0. SET-SIZE(lpDocName) = 0. END PROCEDURE. /* MakeDocument */ PROCEDURE PrintWindow : /*------------------------------------------------------------------------------ Notes: a couple of extra parameters would be nice, like: input leftmargin, rightmargin, Ytop output Ybottom ------------------------------------------------------------------------------*/ define input parameter hdcDest as INTEGER NO-UNDO. /* Printer */ define input parameter hWin as INTEGER NO-UNDO. /* windows handle to progress window to print */ DEFINE VARIABLE hdcWin as integer no-undo. /* hdc window */ DEFINE VARIABLE PrintHorzRes as integer no-undo. /* printer resolution */ DEFINE VARIABLE PrintVertRes as integer no-undo. DEFINE VARIABLE WinHorzRes as integer no-undo. /* display resolution */ DEFINE VARIABLE WinVertRes as integer no-undo. DEFINE VARIABLE hdcComp as integer no-undo. /* hdc memory */ DEFINE VARIABLE hbmpComp as integer no-undo. /* bitmap in memory */ DEFINE VARIABLE hbmpDest as integer no-undo. /* bitmap on paper */ DEFINE VARIABLE WinWidth as INTEGER NO-UNDO. /* dimensions of window */ DEFINE VARIABLE WinHeight as INTEGER NO-UNDO. DEFINE VARIABLE PictureWidth as integer no-undo. /* dimensions of picture on paper */ DEFINE VARIABLE PictureHeight as integer no-undo. DEFINE VARIABLE Scale as decimal no-undo. DEFINE VARIABLE xMargin as INTEGER NO-UNDO. /* center picture horizontally */ DEFINE VARIABLE numrows as integer no-undo. /* split large pics into rows/cols */ DEFINE VARIABLE numcols as integer no-undo. DEFINE VARIABLE rw as integer no-undo. DEFINE VARIABLE cl as integer no-undo. DEFINE VARIABLE lpRect as MEMPTR NO-UNDO. DEFINE VARIABLE ReturnValue as integer no-undo. DEFINE VARIABLE lpOrigin as MEMPTR NO-UNDO. IF getParent THEN RUN GetParent(hWin, OUTPUT hWin). /* get the window resolution */ run GetDC (hWin, output hdcWin). run GetDeviceCaps(hdcWin, 8 /* = HorzRes */, output WinHorzRes). run GetDeviceCaps(hdcWin, 10 /* = VertRes */, output WinVertRes). /* get the printer resolution */ run GetDeviceCaps(hdcDest, 8 /* = HorzRes */, output PrintHorzRes). run GetDeviceCaps(hdcDest, 10 /* = VertRes */, output PrintVertRes). /* determine dimensions of the window */ /* Also determine the coordinates of the upper-left corner. This is (0,0) for a client window, but will be somewhere around (-4,-20) for a window with titlebar */ SET-SIZE (lpOrigin) = 8. SET-SIZE (lpRect) = 16. RUN GetWindowRect (hWin, GET-POINTER-VALUE(lpRect) , OUTPUT ReturnValue). WinWidth = GET-LONG(lpRect, 9) - GET-LONG(lpRect, 1). WinHeight = GET-LONG(lpRect,13) - GET-LONG(lpRect, 5). PUT-LONG(lpOrigin, 1) = GET-LONG(lpRect, 1). PUT-LONG(lpOrigin, 5) = GET-LONG(lpRect, 5). RUN ScreenToClient (hWin, GET-POINTER-VALUE(lpOrigin), OUTPUT ReturnValue). SET-SIZE (lpRect) = 0. Scale = MINIMUM(PrintHorzRes / WinWidth, PrintVertRes / WinHeight). IF Scale < 1 THEN Scale = 1. run CreateCompatibleDC (hdcWin, output hdcComp). run CreateCompatibleBitmap (hdcComp, integer(winWidth * Scale), integer(winHeight * Scale), output hbmpComp). run CreateCompatibleBitmap (hdcDest, integer(winWidth * Scale), integer(winHeight * Scale), output hbmpDest). run SelectObject (hdcComp, hbmpComp, output ReturnValue). run SelectObject (hdcDest, hbmpDest, output ReturnValue). run StretchBlt (hdcDest, 0, 0, integer(winwidth * Scale) , integer(winHeight * Scale) , hdcWin, get-long(lpOrigin,1), get-long(lpOrigin,5), winwidth, winheight, 13369376, /* = SRCCOPY */ output ReturnValue). /* cleanup */ run ReleaseDC (hWin, hdcWin, output ReturnValue). run DeleteDC (hdcComp, output ReturnValue). run DeleteObject (hbmpComp, output ReturnValue). run DeleteObject (hbmpDest, output ReturnValue). SET-SIZE(lpOrigin)=0. END PROCEDURE. /* PrintWindow */
/* ============================================================== file : printscreen.i by : Jurjen Dijkstra (Modified by Ian Keene Oct 2003) dd : 05/16/1999 purpose : API definitions used in printscreen.p ============================================================== */ &GLOB GDI "gdi32.dll" &GLOB USER "user32.dll" &GLOB BOOL LONG &GLOB COLORREF LONG &GLOB DWORD LONG &GLOB HANDLE LONG &GLOB HDC LONG &GLOB HGDIOBJ LONG &GLOB HWND LONG &GLOB INTEGER LONG &GLOB LONG LONG &GLOB LP LONG &GLOB LPCSTR CHARACTER &GLOB LPCTSTR CHARACTER &GLOB LPSECURITY_ATTRIBUTES LONG &GLOB LPSTRUCT LONG &GLOB LPTSTR CHARACTER &GLOB SHORT SHORT &GLOB UINT LONG &GLOB WORD SHORT PROCEDURE CreateDCA EXTERNAL {&GDI} : DEFINE INPUT PARAMETER lpszDriver AS {&LPCTSTR} NO-UNDO. DEFINE INPUT PARAMETER lpszDevice AS {&LPCTSTR} NO-UNDO. DEFINE INPUT PARAMETER lpszOutput AS {&LP} NO-UNDO. DEFINE INPUT PARAMETER lpInitData AS {&LPSTRUCT} NO-UNDO. DEFINE RETURN PARAMETER hDC AS {&HDC} NO-UNDO. END PROCEDURE. PROCEDURE StartDocA EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE INPUT PARAMETER lpdi AS {&LPSTRUCT} NO-UNDO. DEFINE RETURN PARAMETER JobId AS {&int} NO-UNDO. END PROCEDURE. PROCEDURE StartPage EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&int} NO-UNDO. END PROCEDURE. PROCEDURE EndPage EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&int} NO-UNDO. END PROCEDURE. PROCEDURE EndDoc EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&int} NO-UNDO. END PROCEDURE. PROCEDURE GetParent EXTERNAL {&USER} : DEFINE INPUT PARAMETER hWinHdl AS {&HWND} NO-UNDO. DEFINE RETURN PARAMETER hwndParent AS {&HWND} NO-UNDO. END PROCEDURE. PROCEDURE GetDC EXTERNAL {&USER} : DEFINE INPUT PARAMETER hWinHdl AS {&HWND} NO-UNDO. DEFINE RETURN PARAMETER hdc AS {&HDC} NO-UNDO. END PROCEDURE. PROCEDURE GetDeviceCaps EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE INPUT PARAMETER nIndex AS {&int} NO-UNDO. DEFINE RETURN PARAMETER dwCaps AS {&int} NO-UNDO. END PROCEDURE. PROCEDURE GetWindowRect EXTERNAL {&USER} : DEFINE INPUT PARAMETER hWinHdl AS {&HWND} NO-UNDO. DEFINE INPUT PARAMETER lpRect AS {&LPSTRUCT} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&BOOL} NO-UNDO. END PROCEDURE. PROCEDURE ScreenToClient EXTERNAL {&USER} : DEFINE INPUT PARAMETER hWinHdl AS {&HWND} NO-UNDO. DEFINE INPUT PARAMETER lpPoint AS {&LPSTRUCT} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&BOOL} NO-UNDO. END PROCEDURE. PROCEDURE CreateCompatibleDC EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE RETURN PARAMETER hdcComp AS {&HDC} NO-UNDO. END PROCEDURE. PROCEDURE CreateCompatibleBitmap EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE INPUT PARAMETER nWidth AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nHeight AS {&int} NO-UNDO. DEFINE RETURN PARAMETER hbmp AS {&HANDLE} NO-UNDO. END PROCEDURE. PROCEDURE SelectObject EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE INPUT PARAMETER hgdiobj AS {&HGDIOBJ} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&HGDIOBJ} NO-UNDO. END PROCEDURE. PROCEDURE StretchBlt EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdcDest AS {&HDC} NO-UNDO. DEFINE INPUT PARAMETER nXOriginDest AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nYOriginDest AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nWidthDest AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nHeightDest AS {&int} NO-UNDO. DEFINE INPUT PARAMETER hdcSrc AS {&HDC} NO-UNDO. DEFINE INPUT PARAMETER nXOriginSrc AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nYOriginSrc AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nWidthSrc AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nHeightSrc AS {&int} NO-UNDO. DEFINE INPUT PARAMETER dwRop AS {&DWORD} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&BOOL} NO-UNDO. END PROCEDURE. PROCEDURE BitBlt EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdcDest AS {&HDC} NO-UNDO. DEFINE INPUT PARAMETER nXOriginDest AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nYOriginDest AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nWidthDest AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nHeightDest AS {&int} NO-UNDO. DEFINE INPUT PARAMETER hdcSrc AS {&HDC} NO-UNDO. DEFINE INPUT PARAMETER nXOriginSrc AS {&int} NO-UNDO. DEFINE INPUT PARAMETER nYOriginSrc AS {&int} NO-UNDO. DEFINE INPUT PARAMETER dwRop AS {&DWORD} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&BOOL} NO-UNDO. END PROCEDURE. PROCEDURE ReleaseDC EXTERNAL {&USER} : DEFINE INPUT PARAMETER hWinHdl AS {&HWND} NO-UNDO. DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&BOOL} NO-UNDO. END PROCEDURE. PROCEDURE DeleteDC EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hdc AS {&HDC} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&BOOL} NO-UNDO. END PROCEDURE. PROCEDURE DeleteObject EXTERNAL {&GDI} : DEFINE INPUT PARAMETER hObject AS {&HGDIOBJ} NO-UNDO. DEFINE RETURN PARAMETER ReturnValue AS {&BOOL} NO-UNDO. END PROCEDURE. PROCEDURE GetProfileStringA EXTERNAL "kernel32.dll": DEFINE INPUT PARAMETER in-appname AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER in-keyname AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER in-default AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER in-ret-str AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER in-n-size AS LONG NO-UNDO. DEFINE RETURN PARAMETER out-nuchr AS LONG NO-UNDO. END PROCEDURE. PROCEDURE OpenPrinterA EXTERNAL "winspool.drv": DEFINE INPUT PARAMETER in-prtname AS CHARACTER NO-UNDO. DEFINE OUTPUT PARAMETER out-hwnd AS LONG NO-UNDO. DEFINE INPUT PARAMETER in-def AS LONG NO-UNDO. END PROCEDURE.
Based on an idea by Richard Gordon.
The question was: how to get a list of available printers and set a new default printer. Here is a solution that uses the win.ini file.
It is recommended for a 32-bit application to use the EnumPrinters procedure to get a list of available printers.
One might think it's not modern to change the default printer by writing in win.ini but this is still the recommended way according to MS documentation.
{windows.i} DEFINE VARIABLE list-of-printers AS CHARACTER. DEFINE VARIABLE newdefault AS CHARACTER. DEFINE VARIABLE driver-and-port AS CHARACTER. RUN getkey (INPUT "devices", /* The section name */ INPUT "", /* The key name */ INPUT "win.ini", /* Name of ini file */ OUTPUT list-of-printers). /* Returned stuff */ /* you now have a comma separated list of printer names. Check it: */ /* message list-of-printers view-as alert-box. */ /* allow the user to pick one, suppose he picks the third entry: */ newdefault = ENTRY(3, list-of-printers). /* read driver and port for the new default printer */ RUN getkey (INPUT "devices", /* The section name */ INPUT newdefault, /* The key name */ INPUT "win.ini", /* Name of ini file */ OUTPUT driver-and-port). /* Returned stuff */ /* and write it back */ RUN putkey (INPUT "windows", INPUT "device", INPUT "win.ini", INPUT newdefault + "," + driver-and-port). /* check it: message session:printer-name view-as alert-box. */ RETURN. /* ------------- internal procedures ------------ */ PROCEDURE getkey : DEFINE INPUT PARAMETER i-section AS CHARACTER. DEFINE INPUT PARAMETER i-key AS CHARACTER. DEFINE INPUT PARAMETER i-filename AS CHARACTER. DEFINE OUTPUT PARAMETER o-value AS CHARACTER. DEFINE VARIABLE EntryPointer AS INTEGER NO-UNDO. DEFINE VARIABLE mem1 AS MEMPTR NO-UNDO. DEFINE VARIABLE mem2 AS MEMPTR NO-UNDO. DEFINE VARIABLE mem1size AS INTEGER NO-UNDO. DEFINE VARIABLE mem2size AS INTEGER NO-UNDO. DEFINE VARIABLE i AS INTEGER NO-UNDO. DEFINE VARIABLE cbReturnSize AS INTEGER NO-UNDO. ASSIGN SET-SIZE(mem1) = 4000 mem1size = 4000. IF i-key = "" THEN EntryPointer = 0. ELSE DO: /* Must fill memory with desired key name and EntryPointer must point to it */ ASSIGN SET-SIZE(mem2) = 128 mem2size = 128 EntryPointer = GET-POINTER-VALUE(mem2) PUT-STRING(mem2,1) = i-key. END. RUN getprivateprofilestring{&A} IN hpApi (i-section, EntryPointer, "", GET-POINTER-VALUE(mem1), INPUT mem1size, i-filename, OUTPUT cbReturnSize). /* if i-key was "", Windows will return a list of all keys in i-section. This list is not comma-separated but separated by CHR(0). Progress can not handle that easily so we'll now replace every 0 by a comma: */ DO i = 1 TO cbReturnSize: /* If this is a list convert null character into a comma to generate a csv type variable */ o-value = IF (GET-BYTE(mem1, i) = 0 AND i NE cbReturnSize) THEN o-value + "," ELSE o-value + CHR(GET-BYTE(mem1, i)). END. SET-SIZE(mem1) = 0. SET-SIZE(mem2) = 0. END PROCEDURE. PROCEDURE putkey : DEFINE INPUT PARAMETER i-section AS CHARACTER. DEFINE INPUT PARAMETER i-key AS CHARACTER. DEFINE INPUT PARAMETER i-filename AS CHARACTER. DEFINE INPUT PARAMETER i-value AS CHARACTER. DEFINE VARIABLE cbReturnSize AS INTEGER. RUN writeprivateprofilestring{&A} IN hpApi (i-section, i-key, i-value, i-filename, OUTPUT cbReturnSize ). END PROCEDURE.
By Nenad Orlovic
Nenad Orlovic writes: "Here is another example for same purpose that uses SetPrinter API function. I don't know which way is better. Both have the same problem, they can't change SESSION:PRINTER-NAME when it was already changed by SYSTEM-DIALOG PRINTER-SETUP command."
usage:
run SetDefaultPrinter("HP LaserJet 4L").
Additional note from Jurjen: Microsoft knowledgebase article Q140560 says this SetPrinter() call does not work on Windows NT4. Probably because NT4 does not support the PRINTER_INFO_5 structure. The good news is: Windows 2000 finally has the new API function we have all been waiting for: "SetDefaultPrinterA". The usage of this new API function is exactly similiar to the SetDefaultPrinter procedure by Nenad.
I did not try but it may be a good idea to call SendNotifyMessage(HWND_BROADCAST,WM_SETTINGCHANGE,0,"windows") to solve the SESSION:PRINTER-NAME problem.
/* Nenad Orlovic */ /* Change default printer example */ FUNCTION CheckBit RETURNS LOGICAL ( INPUT ip_dword AS INT, INPUT ip_bit AS INTEGER ) : /*------------------------------------------------------------------------------ Purpose: Checks if ip_bit in ip_dword is set Notes: ip_bit = 0,1,...,31 ------------------------------------------------------------------------------*/ DEFINE VARIABLE iBit AS INTEGER EXTENT 32 INIT [ 1 ,2 ,4 ,8 ,16 ,32 ,64 ,128, 256 ,512 ,1024 ,2048 ,4096 ,8192 ,16384 ,32768, 65536 ,131072 ,262144 ,524288 ,1048576 ,2097152 ,4194304 ,8388608, 16777216,33554432,67108864,134217728,268435456,536870912,1073741824,-2147483648 ] NO-UNDO. IF ip_bit > 31 OR ip_bit < 0 then return ?. ip_dword = TRUNC(ip_dword / iBit[ip_bit + 1],0). RETURN (ip_dword MOD 2 = 1). END FUNCTION. PROCEDURE SetDefaultPrinter: /*------------------------------------------------------------------------------ Purpose: Sets ip_printer to be windows default printer Parameters: input - Printer name Notes: ------------------------------------------------------------------------------*/ DEFINE INPUT PARAMETER ip_printer AS CHARACTER NO-UNDO. DEFINE VARIABLE iRet AS INTEGER NO-UNDO. DEFINE VARIABLE pPrinter AS MEMPTR NO-UNDO. DEFINE VARIABLE hPrinter AS INTEGER NO-UNDO. DEFINE VARIABLE iSize AS INTEGER NO-UNDO. DEFINE VARIABLE iAttr AS INTEGER NO-UNDO. DEFINE VARIABLE cMessage AS CHARACTER NO-UNDO. RUN OpenPrinterA (ip_printer,OUTPUT hPrinter,0, OUTPUT iRet). IF iRet = 0 THEN cMessage = "ERROR: OpenPrinterA " + ip_printer. ELSE DO: SET-SIZE(pPrinter) = 1. /* First call is only to get needed size for pPrinter */ RUN GetPrinterA(hPrinter,5,pPrinter,1,OUTPUT iSize,OUTPUT iRet). SET-SIZE(pPrinter) = 0. SET-SIZE(pPrinter) = iSize. RUN GetPrinterA(hPrinter,5,pPrinter,iSize,OUTPUT iSize,OUTPUT iRet). /* pPrinter points to PRINTER_INFO_5 */ IF iRet = 0 THEN cMessage = "ERROR: GetPrinterA " + ip_printer. ELSE DO: iAttr = GET-LONG(pPrinter,9). IF NOT CheckBit(iAttr,2) THEN DO: PUT-LONG(pPrinter,9) = iAttr + 4. /* 4 = PRINTER_ATTRIBUTE_DEFAULT */ RUN SetPrinterA(hPrinter,5,pPrinter,0,OUTPUT iRet). IF iRet = 0 THEN cMessage = "ERROR: SetPrinterA " + ip_printer. END. RUN ClosePrinter(hPrinter,OUTPUT iRet). IF iRet = 0 THEN cMessage = cMessage + "ERROR: ClosePrinter " + ip_printer. END. END. SET-SIZE(pPrinter) = 0. RETURN cMessage. END PROCEDURE. PROCEDURE OpenPrinterA EXTERNAL "WINSPOOL.DLL": DEFINE INPUT PARAMETER pPrinterName AS CHARACTER. DEFINE OUTPUT PARAMETER phPrinter AS LONG. DEFINE INPUT PARAMETER pDefault AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE ClosePrinter EXTERNAL "WINSPOOL.DLL": DEFINE INPUT PARAMETER hPrinter AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE SetPrinterA EXTERNAL "WINSPOOL.DLL": DEFINE INPUT PARAMETER hPrinter AS LONG. DEFINE INPUT PARAMETER Level AS LONG. DEFINE INPUT PARAMETER pPrinter AS MEMPTR. DEFINE INPUT PARAMETER COMMAND AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE. PROCEDURE GetPrinterA EXTERNAL "WINSPOOL.DLL": DEFINE INPUT PARAMETER hPrinter AS LONG. DEFINE INPUT PARAMETER Level AS LONG. DEFINE INPUT PARAMETER pPrinter AS MEMPTR. DEFINE INPUT PARAMETER cbBuf AS LONG. DEFINE OUTPUT PARAMETER pcbNeeded AS LONG. DEFINE RETURN PARAMETER X AS LONG. END PROCEDURE.
by Johan Bouduin
The next procedure calls the properties dialog for any available printer.
The properties dialog is more or less part of the printer driver, so the appearance will be different for each printer.
It's only tested on Win95.
/******************************************************************** name : prg/de/de_prop.p author : Johan Bouduin date : 13/02/1998 purpose : get printers syntax : run prg/de/de_prop.p parameters : input PC_PRINTER_NAME as character input PH_CALLER as handle internal procedures : PROC_FREEMEM : de-allocate reserved memory internal functions : external functions : OpenPrinter "winspool.drv" ClosePrinter "winspool.drv" PrinterProperties "winspool.drv" modifications : Jurjen moved external procedures to windows.i/p *********************************************************************/ /***** Parameter definitions ****************************************/ DEFINE INPUT PARAMETER PC_PRINTER_NAME AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER PH_CALLER AS HANDLE NO-UNDO. /***** Variable definitions *****************************************/ DEFINE VARIABLE VM_PRINTER_HANDLE AS MEMPTR NO-UNDO. DEFINE VARIABLE VI_RETURN_VALUE AS INTEGER NO-UNDO. DEFINE VARIABLE VM_PRINTER_NAME AS MEMPTR NO-UNDO. DEFINE VARIABLE VM_DEFAULTS AS MEMPTR NO-UNDO. /***** External procedures ******************************************/ {windows.i} /***** Main-block ***************************************************/ DO: /***** get a printer handle ***************************************/ SET-SIZE(VM_PRINTER_HANDLE) = 4. RUN OpenPrinter{&A} IN hpApi( INPUT PC_PRINTER_NAME, INPUT GET-POINTER-VALUE(VM_PRINTER_HANDLE), INPUT GET-POINTER-VALUE(VM_DEFAULTS), OUTPUT VI_RETURN_VALUE). IF VI_RETURN_VALUE EQ 0 THEN DO: MESSAGE "An error occurred while trying to open the printer" VIEW-AS ALERT-BOX. RETURN "not open". END. /***** call printerproperties *************************************/ RUN PrinterProperties IN hpApi( INPUT PH_CALLER:HWND, INPUT GET-LONG(VM_PRINTER_HANDLE,1), OUTPUT VI_RETURN_VALUE). /***** close the printer ******************************************/ RUN ClosePrinter IN hpApi( INPUT GET-LONG(VM_PRINTER_HANDLE,1), OUTPUT VI_RETURN_VALUE). IF VI_RETURN_VALUE EQ 0 THEN DO: MESSAGE "An error occurred while trying to close the printer" VIEW-AS ALERT-BOX. RETURN "not closed". END. RUN PROC_FREEMEM. RETURN. END. /***** internal procedures ******************************************/ PROCEDURE PROC_FREEMEM: SET-SIZE(VM_PRINTER_HANDLE) = 0. END.
Suppose you have used the EnumPrinters function to populate a selection list widget (SELECT-1) with names of all available printers:
On choose of the button do:
run prg/de/de_prop.p (select-1:screen-value, c-win:handle).
The first parameter is the name of the printer, the second parameter is the Progress widget handle of the current window. This window will be the parent window for the property dialog; you will not be able to activate c-win while the property dialog is opened.
.
by Jurjen Dijkstra and Edwin van Elk
When you look at the "Applications" tab in Windows Task Manager, you see that every Progress session has the same title and icon. When you run multiple Progress sessions you may wish to change the icon and/or title of each individual entry in this list.
The Progress session creates one hidden window, which is the owner of all other Progress window. This ultimate owner is the window whos icon and title are displayed in the Task Manager. There is no Progress widget for this window, so you need API functions in order to manipulate it.
&Scoped-Define WM_GETICON 127 &Scoped-Define WM_SETICON 128 /* WM_SETICON / WM_GETICON Type Codes */ &Scoped-Define ICON_SMALL 0 &Scoped-Define ICON_BIG 1 /* some GetWindow() Constants */ &Scoped-Define GW_OWNER 4 DEFINE VARIABLE hParent AS INTEGER NO-UNDO. DEFINE VARIABLE hOwner AS INTEGER NO-UNDO. DEFINE VARIABLE i_ApiStat AS INTEGER NO-UNDO. DEFINE VARIABLE hIcon AS INTEGER NO-UNDO. /* find the hidden owner window */ RUN GetParent (DEFAULT-WINDOW:HWND, OUTPUT hParent). RUN GetWindow (hParent, {&GW_OWNER}, OUTPUT hOwner). /* change the title: */ RUN SetWindowTextA (hOwner, "This is the new application title"). /* change the icon: */ RUN ExtractIconA (0, SEARCH("ICON.ICO":U), 0, OUTPUT hIcon). IF hIcon > 0 THEN DO: RUN SendMessageA( hOwner, {&WM_SETICON}, {&ICON_BIG}, hIcon, OUTPUT i_ApiStat ). RUN SendMessageA( hOwner, {&WM_SETICON}, {&ICON_SMALL}, hIcon, OUTPUT i_ApiStat ). END. /* ----------- API definitions: ----------------------- */ PROCEDURE SetWindowTextA EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER txt AS CHARACTER. END PROCEDURE. PROCEDURE SendMessageA EXTERNAL "USER32.DLL": DEFINE INPUT PARAMETER h_Widget AS LONG. DEFINE INPUT PARAMETER i_Message AS LONG. DEFINE INPUT PARAMETER i_wParam AS LONG. DEFINE INPUT PARAMETER i_lParam AS LONG. DEFINE RETURN PARAMETER i_ApiStatus AS LONG. END PROCEDURE. PROCEDURE GetWindow EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER uCmd AS LONG. DEFINE RETURN PARAMETER hwndOther AS LONG. END PROCEDURE. PROCEDURE GetParent EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER hwndChild AS LONG. DEFINE RETURN PARAMETER hwndParent AS LONG. END PROCEDURE. PROCEDURE ExtractIconA EXTERNAL "shell32.dll": DEFINE INPUT PARAMETER hInst AS LONG. DEFINE INPUT PARAMETER lpszExeFileName AS CHARACTER. DEFINE INPUT PARAMETER nIconIndex AS LONG. DEFINE RETURN PARAMETER i_Return AS LONG. END PROCEDURE.
If you only want to set the BIG icon you don't need API functions:
SESSION:LOAD-ICON ("example.ico").
However, the BIG icon does not affect the Task Manager listview. It only affects the Alt-Tab window.
_Based on an example from Microsofts whitepaper 'Optimizing Applications for Windows NT Server Terminal Server Edition, version 4.0'_
Suppose you want to prevent your Progress application to be launched more than once on each computer. The startup procedure would contain something like this:
IF IsAppAlreadyRunning(NO, "MyProgressApplication") THEN DO: MESSAGE "'MyProgressApplication' is already running on this machine," SKIP "only one instance is allowed." VIEW-AS ALERT-BOX. QUIT. END. ... RUN LetAnotherInstanceRun("MyProgressApplication").
There are several ways to implement this functionality. This topic will use a mutex.
A mutex is an object that can only be owned by one thread at a time. The general purpose of a mutex is to synchronize threads, ie to have other threads wait until the mutex is released. So if your application creates and owns a named mutex, other applications can not get ownership of the same mutex. Function IsAppAlreadyRunning creates a named mutex, procedure LetAnotherInstanceRun closes the mutex.
{windows.i} DEFINE VARIABLE hAppRunningMutex AS INTEGER NO-UNDO INITIAL 0. FUNCTION IsAppAlreadyRunning RETURN LOGICAL (p-OnePerSystem AS LOGICAL, p-AppName AS CHARACTER): DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE MutexName AS CHARACTER NO-UNDO. MutexName = ''. IF p-OnePerSystem AND ValidateProductSuite("Terminal Server") THEN MutexName = MutexName + "Global\". MutexName = MutexName + p-AppName + ' is running'. RUN CreateMutexA IN hpApi(0,0,MutexName, OUTPUT hAppRunningMutex). IF hAppRunningMutex NE 0 THEN DO: /* we should check GetLastError = ERROR_ALREADY_EXISTS, but unfortunately GetLastError doesn't work with Progress until 9.0B */ /* Instead we will try to get ownership of the Mutex. This will be easy if we created the mutex, but will be impossible if another instance created the mutex (and still holds ownership) */ RUN WaitForSingleObject IN hpApi (hAppRunningMutex,100, OUTPUT ReturnValue). IF NOT (ReturnValue={&WAIT_ABANDONED} OR ReturnValue={&WAIT_OBJECT_0}) THEN DO: RUN CloseHandle IN hpApi(hAppRunningMutex, OUTPUT ReturnValue). hAppRunningMutex = 0. END. END. RETURN (hAppRunningMutex=0). END.
The first parameter, p-OnePerSystem specifies if the application is allowed to run more than once per system. This is useful when the application is installed on Microsoft Windows Terminal Server hosting multiple users. If p-OnePerSystem=No, the application can be launched once by each user. If p-OnePerSystem=Yes the application can run only once on the entire Terminal Server system, in other words: by only one user at a time. This might be useful for batch processes perhaps?
Procedure LetAntotherInstanceRun closes the mutex, making it available to other threads. This decreases the usage-count of the mutex. If the usage-count decreases to zero (like now) the mutex will be deleted. It is not very important to run this procedure because the mutex will be closed automatically by Windows when the Progress session quits.
PROCEDURE LetAnotherInstanceRun : DEFINE INPUT PARAMETER p-AppName AS CHARACTER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. IF hAppRunningMutex NE 0 THEN DO: RUN CloseHandle IN hpApi(hAppRunningMutex, OUTPUT ReturnValue). hAppRunningMutex = 0. END. END PROCEDURE.
Function ValidateProductSuite checks if the application is installed on and running on a Windows Terminal Server machine :
{windows.i} FUNCTION ValidateProductSuite RETURN LOGICAL (SuitName AS CHARACTER): DEFINE VARIABLE key-hdl AS INTEGER NO-UNDO. DEFINE VARIABLE lpBuffer AS MEMPTR NO-UNDO. DEFINE VARIABLE lth AS INTEGER NO-UNDO. DEFINE VARIABLE datatype AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE retval AS LOGICAL NO-UNDO INITIAL FALSE. RUN RegOpenKeyA IN hpApi ( {&HKEY_LOCAL_MACHINE}, "System\CurrentControlSet\Control\ProductOptions", OUTPUT key-hdl, OUTPUT ReturnValue). IF ReturnValue NE {&ERROR_SUCCESS} THEN RETURN FALSE. /* make buffer large enough The maximum size is supposed to be MAX_PATH + 1 */ ASSIGN lth = {&MAX_PATH} + 1 SET-SIZE(lpBuffer) = lth. RUN RegQueryValueExA IN hpApi ( key-hdl, "ProductSuite", 0, /* reserved, must be 0 */ OUTPUT datatype, GET-POINTER-VALUE(lpBuffer), INPUT-OUTPUT lth, OUTPUT ReturnValue). IF ReturnValue = {&ERROR_SUCCESS} THEN retval = (GET-STRING(lpBuffer,1)=SuitName). SET-SIZE(lpBuffer)=0. IF key-hdl NE 0 THEN RUN RegCloseKey IN hpApi (key-hdl,OUTPUT ReturnValue). RETURN retval. END FUNCTION.
This procedure can be used to show a list of all running processes, for example to see if Netscape.exe is running. The process indentifier (pid) can be used for getting additional information about the process, or for terminating the process.
This method uses the psapi.dll which only works on NT (and Windows 2000 etc). On Windows 95 or Windows 98 you can not use psapi.dll, instead you can use the much nicer CreateToolhelp32 functions.
To check if you are running Windows NT4.0 see page: which version of Windows is running.
FUNCTION GetProcessName RETURNS CHARACTER (INPUT PID AS INTEGER) : DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. DEFINE VARIABLE cbNeeded AS INTEGER NO-UNDO. DEFINE VARIABLE lphMod AS MEMPTR NO-UNDO. DEFINE VARIABLE szProcessName AS CHARACTER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. /* OpenProcess returns a handle (hProcess), needed for querying info about the process */ RUN OpenProcess ( {&PROCESS_QUERY_INFORMATION} + {&PROCESS_VM_READ}, 0, PID, OUTPUT hProcess). /* some system processes can not be queried, like "System" and "System Idle Process" and "csrss.exe". ProcessName will be initialized to [unknown] for these processes: */ szProcessName = "[unknown]" + FILL(" ", {&MAX_PATH}). IF hProcess NE 0 THEN DO: /* EnumProcessModules fills an array of module handles */ /* The first module handle is a handle to the main module, and that's the only handle you need */ SET-SIZE (lphMod) = 4. /* need only one hMod */ RUN EnumProcessModules ( hProcess, GET-POINTER-VALUE(lphMod), GET-SIZE(lphMod), OUTPUT cbNeeded, OUTPUT ReturnValue). IF ReturnValue NE 0 THEN DO: /* GetModuleBaseNameA returns the name of a module. Because this module is the main module, it's also considered to be the name of the process */ RUN GetModuleBaseNameA (hProcess, GET-LONG(lphMod,1), OUTPUT szProcessName, LENGTH(szProcessName), OUTPUT ReturnValue). /* ReturnValue is the number of returned bytes (chars): */ szProcessName = SUBSTRING(szProcessName,1,ReturnValue). SET-SIZE (lphMod) = 0. END. RUN CloseHandle ( hProcess, OUTPUT ReturnValue). END. RETURN TRIM(szProcessName). END FUNCTION. /* =============== TEST ================ */ DEFINE VARIABLE lpId AS MEMPTR NO-UNDO. DEFINE VARIABLE PID AS INTEGER NO-UNDO. DEFINE VARIABLE cbNeeded AS INTEGER NO-UNDO. DEFINE VARIABLE i AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. /* lpID is an array of PID's (Process Identifiers) */ SET-SIZE(lpId) = 1000. /* assume room for 250 pid's */ /* EnumProcesses fills an array of PID's */ RUN EnumProcesses (INPUT GET-POINTER-VALUE(lpId), INPUT GET-SIZE(lpID), OUTPUT cbNeeded, OUTPUT ReturnValue). DO i = 1 TO cbNeeded / 4 : PID = GET-LONG(lpID, 4 * (i - 1) + 1). /* display what you have found (for testing purposes) */ MESSAGE 'PID=' PID SKIP 'Name=' GetProcessName(PID) VIEW-AS ALERT-BOX. END. SET-SIZE(lpId) = 0.
Definitions used in this procedure:
&GLOB PROCESS_QUERY_INFORMATION 1024 &GLOB PROCESS_VM_READ 16 &GLOB MAX_PATH 260 PROCEDURE EnumProcesses EXTERNAL "psapi.dll" : DEFINE INPUT PARAMETER lpIdProcess AS LONG. DEFINE INPUT PARAMETER cb AS LONG. DEFINE OUTPUT PARAMETER cbNeeded AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE EnumProcessModules EXTERNAL "psapi.dll" : DEFINE INPUT PARAMETER hProcess AS LONG. DEFINE INPUT PARAMETER lphModule AS LONG. /* lp to array of module handles */ DEFINE INPUT PARAMETER cb AS LONG. DEFINE OUTPUT PARAMETER cbNeeded AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE GetModuleBaseNameA EXTERNAL "psapi.dll" : DEFINE INPUT PARAMETER hProcess AS LONG. DEFINE INPUT PARAMETER hModule AS LONG. DEFINE OUTPUT PARAMETER lpBaseName AS CHARACTER. DEFINE INPUT PARAMETER nSize AS LONG. DEFINE RETURN PARAMETER nReturnedSize AS LONG. END PROCEDURE. PROCEDURE OpenProcess EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER dwDesiredAccess AS LONG. DEFINE INPUT PARAMETER bInheritHandle AS LONG. DEFINE INPUT PARAMETER dwProcessId AS LONG. DEFINE RETURN PARAMETER hProcess AS LONG. END PROCEDURE. PROCEDURE CloseHandle EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hObject AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.
based on an example by Michael Rüsweg-Gilbert
This procedure can be used to show a list of all running processes, for example to see if Netscape.exe is running. The process indentifier (pid) can be used for getting additional information about the process, or for terminating the process. See TerminateProcess.
This method only works on Windows 95, Windows 98 and Windows 2000. For Windows NT4.0 you have to use procedure EnumProcesses instead.
To check if you are running Windows NT4.0 see page: which Windows version is running.
RUN ListProcesses. PROCEDURE ListProcesses: DEFINE VARIABLE hSnapShot AS INTEGER NO-UNDO. DEFINE VARIABLE lpPE AS MEMPTR NO-UNDO. /* PROCESSENTRY32 structure */ DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE list AS CHARACTER NO-UNDO INITIAL "Process-List:". /* Create and open SnapShot-list */ RUN CreateToolhelp32Snapshot({&TH32CS_SNAPPROCESS}, 0, OUTPUT hSnapShot). IF hSnapShot = -1 THEN RETURN. /* init buffer for lpPE */ SET-SIZE(lpPE) = 336. PUT-LONG(lpPE, 1) = GET-SIZE(lpPE). /* Cycle thru process-records */ RUN Process32First(hSnapShot, lpPE, OUTPUT ReturnValue). DO WHILE ReturnValue NE 0: list = list + "~n". /* show process identifier (pid): */ list = list + STRING(GET-LONG(lpPE, 9)) + " ". /* show path and filename of executable: */ list = list + GET-STRING(lpPE, 37). RUN Process32Next(hSnapShot, lpPE, OUTPUT ReturnValue). END. /* Close SnapShot-list */ RUN CloseHandle(hSnapShot, OUTPUT ReturnValue). MESSAGE list VIEW-AS ALERT-BOX. END PROCEDURE.
Definitions used in this procedure:
&GLOB TH32CS_SNAPPROCESS 2 PROCEDURE CreateToolhelp32Snapshot EXTERNAL "kernel32" : DEFINE INPUT PARAMETER dwFlags AS LONG. DEFINE INPUT PARAMETER th32ProcessId AS LONG. DEFINE RETURN PARAMETER hSnapShot AS LONG. END PROCEDURE. PROCEDURE Process32First EXTERNAL "kernel32" : DEFINE INPUT PARAMETER hSnapShot AS LONG. DEFINE INPUT PARAMETER lpProcessEntry32 AS MEMPTR. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE Process32Next EXTERNAL "kernel32" : DEFINE INPUT PARAMETER hSnapShot AS LONG. DEFINE INPUT PARAMETER lpProcessEntry32 AS MEMPTR. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE CloseHandle EXTERNAL "kernel32" : DEFINE INPUT PARAMETER hObject AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.
Does anyone know a good way of getting the memory usage of all running processes.
sourcecode by Michael Rüsweg-Gilbert
Function GetProcessTimes works on Windows NT only.
GetProcessTimes obtains timing information about a specified process: the creation time, exit time, kernel time and user time. All these are returned as FILETIME structures (a 64 bit count of 100-nanosecond units).
Creation time and exit time are expressed as time elapsed since midnight January 1, 1601 (UTC). Function FileTimeToSystemTime converts this to system time - which may also be UTC.
Function FileTimeToLocalFileTime can be called prior to FileTimeToSystemTime if you want the output to be displayed in local time.
Kernel time and user time are amounts of time: the FILETIME structures will contain the amount of 100 nanosecond units (ten million units is one second).
This example uses GetProcessTimes for the current (Progress) process. The exit time is null or random because the current process did not exit yet.
/* ----------------------------------------------------------- // File: tst_procTime.p // Desc: query the process-times of the current process // // Parm: --- // // // Author: Michael Rüsweg-Gilbert // Created: 20. Sept. 1999 -------------------------------------------------------------- */ DEFINE VARIABLE RetVal AS INTEGER NO-UNDO. DEFINE VARIABLE me_Crea AS MEMPTR NO-UNDO. DEFINE VARIABLE me_Exit AS MEMPTR NO-UNDO. DEFINE VARIABLE me_Kern AS MEMPTR NO-UNDO. DEFINE VARIABLE me_User AS MEMPTR NO-UNDO. DEFINE VARIABLE hProc AS INTEGER NO-UNDO. DEFINE VARIABLE PID AS INTEGER NO-UNDO. &GLOB TRUE 1 &GLOB FALSE 0 &GLOB PROCESS_ALL_ACCESS 2035711 /* 0x0F0000 | 0x100000 | 0x000FFF */ /* Convert FileTime into a readable LocalTime-String */ FUNCTION proTimeString RETURNS CHAR ( ip_filetime AS MEMPTR): DEFINE VARIABLE tmp_sysTime AS MEMPTR NO-UNDO. DEFINE VARIABLE Ret AS INTEGER NO-UNDO. DEFINE VARIABLE cTime AS CHARACTER NO-UNDO INIT ?. SET-SIZE(tmp_sysTime) = 16. /* Convert UTC-Time to Local Time */ RUN FileTimeToSystemTime ( INPUT ip_filetime, OUTPUT tmp_systime, OUTPUT Ret ). IF Ret = {&TRUE} THEN DO: /* a DAY.MONTH.YEAR HOUR:MINUTE:SECOND-string */ cTime = STRING(GET-SHORT(tmp_sysTime, 7)) + "." + STRING(GET-SHORT(tmp_sysTime, 3)) + "." + STRING(GET-SHORT(tmp_sysTime, 1)) + " " + STRING(GET-SHORT(tmp_sysTime, 9)) + ":" + STRING(GET-SHORT(tmp_sysTime, 11)) + ":" + STRING(GET-SHORT(tmp_sysTime, 13)). END. SET-SIZE(tmp_sysTime) = 0. IF cTime = ? THEN RETURN "Error in FileTimeToSystemTime; Ret=" + STRING(Ret). ELSE RETURN cTime. END FUNCTION. /* first obtain the current Process Token (add Debug rights) */ RUN GetCurrentProcessId(OUTPUT PID). RUN OpenProcess ( {&Process_All_Access}, 0, PID, OUTPUT hProc). IF hProc LT 1 THEN DO: MESSAGE "Can't open current PID" PID VIEW-AS ALERT-BOX INFO BUTTONS OK. RETURN. END. HProc0: DO: SET-SIZE(me_Crea) = 8. SET-SIZE(me_Exit) = 8. SET-SIZE(me_Kern) = 8. SET-SIZE(me_User) = 8. RUN GetProcessTimes ( hProc, me_Crea, me_Exit, me_Kern, me_User, OUTPUT RetVal). IF RetVal NE {&TRUE} THEN DO: MESSAGE "GetProcessTimes returned" RetVal VIEW-AS ALERT-BOX. LEAVE. END. MESSAGE "Creation Time: " ProTimeString(me_Crea) SKIP " Exit Time: " ProTimeString(me_Exit) SKIP " Kernel Time: " ProTimeString(me_Kern) SKIP " User Time: " ProTimeString(me_User) VIEW-AS ALERT-BOX. END. SET-SIZE(me_Crea) = 0. SET-SIZE(me_Exit) = 0. SET-SIZE(me_Kern) = 0. SET-SIZE(me_User) = 0. RUN CloseHandle ( hProc, OUTPUT RetVal). RETURN. PROCEDURE CloseHandle EXTERNAL "kernel32": DEFINE INPUT PARAMETER hObject AS LONG . DEFINE RETURN PARAMETER retval AS LONG . END PROCEDURE. PROCEDURE GetCurrentProcessId EXTERNAL "kernel32": DEFINE RETURN PARAMETER PID AS LONG . END PROCEDURE. PROCEDURE GetLastError EXTERNAL "kernel32": DEFINE RETURN PARAMETER dwError AS LONG . END PROCEDURE. PROCEDURE OpenProcess EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER dwDesiredAccess AS LONG. DEFINE INPUT PARAMETER bInheritHandle AS LONG. DEFINE INPUT PARAMETER dwProcessId AS LONG. DEFINE RETURN PARAMETER hProcess AS LONG. END PROCEDURE. PROCEDURE GetProcessTimes EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hProcess AS LONG. DEFINE INPUT PARAMETER lpCreationTime AS MEMPTR. /* FILETIME */ DEFINE INPUT PARAMETER lpExitTime AS MEMPTR. /* FILETIME */ DEFINE INPUT PARAMETER lpKernelTime AS MEMPTR. /* FILETIME */ DEFINE INPUT PARAMETER lpUserTime AS MEMPTR. /* FILETIME */ DEFINE RETURN PARAMETER RetBool AS LONG. END PROCEDURE. PROCEDURE FileTimeToSystemTime EXTERNAL "kernel32.dll": DEFINE INPUT PARAMETER lpFileTime AS MEMPTR. /* L = 8 */ DEFINE OUTPUT PARAMETER lpSystemTime AS MEMPTR. /* L = 16 */ DEFINE RETURN PARAMETER retBool AS LONG. /* = 0, if failure */ END PROCEDURE.
It is possible to list all modules (exe, dll, ocx, drv) that are in use by a particular process. This example lists all modules loaded by the current process, which is of course the running Progress process.
The resulting list can be useful during development, to check if a certain DLL or OCX really got released, but can also be useful for support engineers to check if a customer site has the appropriate module versions.
Unfortunately the procedure for Windows NT4 is very different compared to 95/98/2000.
DEFINE TEMP-TABLE module FIELD hModule AS INTEGER FORMAT "->>>>>>>>>>>9" FIELD cntUsage AS INTEGER FIELD ModuleName AS CHARACTER FORMAT "x(20)" FIELD ModulePath AS CHARACTER FORMAT "x(150)" FIELD FileVersion AS CHARACTER FORMAT "x(15)" FIELD ProductVersion AS CHARACTER FORMAT "x(15)" INDEX key_name IS PRIMARY ModuleName. RUN FindModules. /* assuming you want to display the contents of the module temp-table in a browse widget: */ {&OPEN-BROWSERS-IN-QUERY-DEFAULT-FRAME}
PROCEDURE FindModules : FOR EACH module : DELETE module. END. IF RunningWindowsNT4() THEN RUN FindModules_NT4. ELSE RUN FindModules_notNT4. FOR EACH module : RUN GetProductVersion(module.modulePath, OUTPUT module.ProductVersion, OUTPUT module.FileVersion). END. END PROCEDURE.
Windows 9x and Windows 2000 support the fairly new toolhelp procedures for finding process information.
PROCEDURE FindModules_notNT4 : DEFINE VARIABLE hSnapShot AS INTEGER NO-UNDO. DEFINE VARIABLE lpME AS MEMPTR NO-UNDO. /* MODULEENTRY32 structure */ DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. FOR EACH module : DELETE module. END. IF RunningWindowsNT4() THEN DO: MESSAGE "Sorry, this procedure does not work with NT4" VIEW-AS ALERT-BOX. RETURN. END. /* Create and open SnapShot-list */ RUN CreateToolhelp32Snapshot({&TH32CS_SNAPMODULE}, 0, OUTPUT hSnapShot). IF hSnapShot = -1 THEN RETURN. /* init buffer for lpPE */ SET-SIZE(lpME) = 32 + 256 + 260. PUT-LONG(lpME, 1) = GET-SIZE(lpME). /* Cycle thru process-records */ RUN Module32First(hSnapShot, lpME, OUTPUT ReturnValue). DO WHILE ReturnValue NE 0: CREATE module. ASSIGN module.moduleName = GET-STRING(lpME, 33) module.modulePath = GET-STRING(lpME, 33 + 256) module.cntUsage = GET-LONG(lpME, 17) module.hModule = GET-LONG(lpME, 29). RUN Module32Next(hSnapShot, lpME, OUTPUT ReturnValue). END. /* Close SnapShot-list */ RUN CloseHandle(hSnapShot, OUTPUT ReturnValue). END PROCEDURE.
In NT 4 the only way to find process information is by reading the registry in the HK_PERFORMANCE_DATA key. Interpreting the data in this registry interface is very complicated but there is a library, PSAPI.DLL, which contains a couple of higher-level procedures and reads the registry interface for you. PSAPI.DLL does not reveal every possible info from the registry but enough for this purpose.
PROCEDURE FindModules_NT4 : DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE ProcessId AS INTEGER NO-UNDO. DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. DEFINE VARIABLE lphMod AS MEMPTR NO-UNDO. DEFINE VARIABLE hModule AS INTEGER NO-UNDO. DEFINE VARIABLE cbNeeded AS INTEGER NO-UNDO. DEFINE VARIABLE szModuleName AS CHARACTER NO-UNDO. DEFINE VARIABLE szModuleNameEx AS CHARACTER NO-UNDO. DEFINE VARIABLE i AS INTEGER NO-UNDO. RUN GetCurrentProcessId (OUTPUT ProcessId). RUN OpenProcess ( {&PROCESS_QUERY_INFORMATION} + {&PROCESS_VM_READ}, 0, ProcessID, OUTPUT hProcess). /* if process handle for the current process is found, then: */ IF hProcess NE 0 THEN DO: SET-SIZE (lphMod) = 4 * 1024. /* should be more than enough */ RUN EnumProcessModules ( hProcess, GET-POINTER-VALUE(lphMod), GET-SIZE(lphMod), OUTPUT cbNeeded, OUTPUT ReturnValue). IF ReturnValue NE 0 THEN DO: DO i=1 TO cbNeeded / 4 : hModule = GET-LONG(lphMod, (i - 1) * 4 + 1). szModuleName = "" + FILL(" ", {&MAX_PATH}). RUN GetModuleBaseNameA (hProcess, hModule, OUTPUT szModuleName, LENGTH(szModuleName), OUTPUT ReturnValue). /* ReturnValue is the number of returned bytes (chars): */ szModuleName = TRIM(SUBSTRING(szModuleName,1,ReturnValue)). szModuleNameEx = "" + FILL(" ", {&MAX_PATH}). RUN GetModuleFileNameExA (hProcess, hModule, OUTPUT szModuleNameEx, LENGTH(szModuleNameEx), OUTPUT ReturnValue). /* ReturnValue is the number of returned bytes (chars): */ szModuleNameEx = TRIM(SUBSTRING(szModuleNameEx,1,ReturnValue)). CREATE module. ASSIGN module.moduleName = szModuleName module.modulePath = szModuleNameEx module.cntUsage = ? module.hModule = hModule. END. SET-SIZE (lphMod) = 0. END. RUN CloseHandle(hProcess, OUTPUT ReturnValue). END. END PROCEDURE.
Definitions used in this procedure, not listed in windows.p :
&GLOB TH32CS_SNAPMODULE 8 &GLOB PROCESS_QUERY_INFORMATION 1024 &GLOB PROCESS_VM_READ 16 PROCEDURE CreateToolhelp32Snapshot EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER dwFlags AS LONG. DEFINE INPUT PARAMETER th32ProcessId AS LONG. DEFINE RETURN PARAMETER hSnapShot AS LONG. END PROCEDURE. PROCEDURE Module32First EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hSnapShot AS LONG. DEFINE INPUT PARAMETER lpModuleEntry32 AS MEMPTR. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE Module32Next EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hSnapShot AS LONG. DEFINE INPUT PARAMETER lpModuleEntry32 AS MEMPTR. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE EnumProcessModules EXTERNAL "psapi.dll" : DEFINE INPUT PARAMETER hProcess AS LONG. DEFINE INPUT PARAMETER lphModule AS LONG. /* lp to array of module handles */ DEFINE INPUT PARAMETER cb AS LONG. DEFINE OUTPUT PARAMETER cbNeeded AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE GetModuleBaseNameA EXTERNAL "psapi.dll" : DEFINE INPUT PARAMETER hProcess AS LONG. DEFINE INPUT PARAMETER hModule AS LONG. DEFINE OUTPUT PARAMETER lpBaseName AS CHARACTER. DEFINE INPUT PARAMETER nSize AS LONG. DEFINE RETURN PARAMETER nReturnedSize AS LONG. END PROCEDURE. PROCEDURE GetModuleFileNameExA EXTERNAL "psapi.dll" : DEFINE INPUT PARAMETER hProcess AS LONG. DEFINE INPUT PARAMETER hModule AS LONG. DEFINE OUTPUT PARAMETER lpFileName AS CHARACTER. DEFINE INPUT PARAMETER nSize AS LONG. DEFINE RETURN PARAMETER nReturnedSize AS LONG. END PROCEDURE.
Function RunningWindowsNT4( ) is covered on page which version of Windows is running.
Procedure GetProductVersion(..) is covered on page File version information.
If you only want to find the path and name of the the current Progress executable module ("prowin32.exe") it is much more convenient to call GetModuleFileName.
by Todd G. Nist
Program source is available for download: w-findservice.w
This is a program for an NT environment which will determine all of the computers on a given network and which services they are running. You can then inquire of a given server what the status is of a services and it will return weather it is running, in error, etc...
It has only been tested under NT 4.0 with service pack 3. You will have to be logged into and authenticated on the network in order to inquire of the status of services running on other machines in the network.
API-procedures used in this example are listed here to be included in the search index: PROCEDURE CloseServiceHandle EXTERNAL "advapi32.dll" PROCEDURE EnumServicesStatusA EXTERNAL "advapi32.dll" PROCEDURE OpenSCManagerA EXTERNAL "advapi32.dll" PROCEDURE OpenServiceA EXTERNAL "advapi32.dll" PROCEDURE QueryServiceConfigA EXTERNAL "advapi32.dll" PROCEDURE QueryServiceStatus EXTERNAL "advapi32.dll" PROCEDURE NetServerEnum EXTERNAL "Netapi32.dll" PROCEDURE NetApiBufferFree EXTERNAL "Netapi32.dll" PROCEDURE lstrcpyW EXTERNAL "kernel32.dll" PROCEDURE lstrlen EXTERNAL "kernel32.dll" PROCEDURE RtlMoveMemory EXTERNAL "kernel32.dll" PROCEDURE WideCharToMultiByte EXTERNAL "kernel32.dll" PROCEDURE GetComputerNameA EXTERNAL "kernel32.dll"
w-findservice.w.zip : example program
The P4GL PAUSE function can only be used for whole seconds, not fractions of seconds.
A loop using the ETIME function can be used to wait for fractions of a second, but will keep the processor busy in the current thread.
The following call will wait for 0.5 seconds and minimize system load :
/* by Michael Rüsweg-Gilbert */ RUN sleep ( 500 ). PROCEDURE Sleep EXTERNAL "KERNEL32": DEFINE INPUT PARAMETER lMilliseconds AS LONG NO-UNDO. END PROCEDURE.
Windows works multi-tasking, sort of. A thread is allowed to work for a certain time quantum. When that quantum is over, the running thread is suspended and one of the other threads can start its own time quantum. Which thread? Well, that is decided based on priorities and is not easy to understand, but one thing is clear: a thread is skipped when it has requested a Sleep.
As a matter of fact, the time quantum for the running thread will immediately be suspended when the thread calls Sleep.
In other words: Sleep gives extra time to other threads.
Sometimes you see Sleep(0) in source code. Sleep(0) does not take very long, it just gives the remainder of the current time quantum back to the operating system. Each of the other threads will have a turn (well, I am ignoring priority issues here) before the thread who called Sleep(0) will execute again.
So Sleep(0) can be useful when you need an other thread to respond to one of your actions.
A window **has to** respond to messages within a fair amount of time, that's one of the rules of the GUI system. That is to say, the window has to be able to repaint itself and respond swiftly to user actions and system messages. A sleeping thread does not respond. In other words, a thread that owns windows should not sleep too long. More precisely: a thread that directly or indirectly creates windows. This also includes threads involved in DDE.
Somewhat off-topic: a thread that owns windows should also not do things like
FOR EACH order: DELETE order. END.
without PROCESS EVENTS inside the loop. Such actions should be performed by a second thread while the GUI thread continues. Oh well.
Topic TerminateProcess introduced the equivalent to the Unix "kill -9" command.
The following 4GL procedure KillProcess(pid) also terminates a process, but tries to avoid the use of TerminateProcess.
Procedure CloseProcessWindows is based on API-function EnumWindows. This API-function can not be called from within P4GL because it needs a callback, so I wrote procedure CloseProcessWindows in Pascal and added it to proextra.dll (see page ProExtra.dll). Of course I might as well have included all the rest in Pascal too, but then I would not allow myself to post it on this Progress site :-)
By the way, the topic on CreateProcess shows how to create a process and return a PID.
{windows.i} {proextra.i} /* version August 21, 1999 */ &GLOBAL-DEFINE PROCESS_QUERY_INFORMATION 1024 &GLOBAL-DEFINE PROCESS_TERMINATE 1 &GLOBAL-DEFINE STILL_ACTIVE 259 /* ======================================================= IsProcessRunning Returns TRUE if the process is not terminated. (also returns TRUE if the process is hanging) ------------------------------------------------------- */ FUNCTION IsProcessRunning RETURNS LOGICAL (PID AS INTEGER) : DEFINE VARIABLE IsRunning AS LOGICAL NO-UNDO INITIAL NO. DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. DEFINE VARIABLE ExitCode AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. RUN Sleep IN hpApi (0). /* Sleep(0) just gives the remainder of this thread's time quantum back to the task switcher so the other process gets the opportunity to finish and release itself. */ RUN OpenProcess IN hpApi ( {&PROCESS_QUERY_INFORMATION}, 0, PID, OUTPUT hProcess). IF hProcess NE 0 THEN DO: RUN GetExitcodeProcess IN hpApi ( hProcess, OUTPUT ExitCode, OUTPUT ReturnValue). IsRunning = (ExitCode={&STILL_ACTIVE}) AND (ReturnValue NE 0). RUN CloseHandle IN hpApi(hProcess, OUTPUT ReturnValue). END. RETURN IsRunning. END FUNCTION. /* ======================================================= KillProcess terminates a process as gently as possible. pHow tells you how it is done, for debugging purposes ------------------------------------------------------- */ PROCEDURE KillProcess : DEFINE INPUT PARAMETER PID AS INTEGER NO-UNDO. DEFINE OUTPUT PARAMETER pHow AS CHARACTER NO-UNDO. DEFINE VARIABLE cName AS CHARACTER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE ProcessHandle AS INTEGER NO-UNDO. /* first step: */ /* ------------ */ /* verify if the process is really running */ pHow='not running'. IF NOT IsProcessRunning(PID) THEN RETURN. /* second step: */ /* ------------ */ /* does the process have windows? If it does, the nicest way to stop the process is send a WM_CLOSE message to each window, as if a human operator pressed the [x]-titlebar button. */ /* If the process is very young it might not have created a window yet. Use WaitForInputIdle to wait until the process has a window and is ready to receive messages. */ pHow='close'. RUN OpenProcess IN hpApi({&PROCESS_QUERY_INFORMATION}, 0, PID, OUTPUT ProcessHandle). IF ProcessHandle NE 0 THEN RUN WaitForInputIdle IN hpApi(ProcessHandle, 1000, /* one second maximum */ OUTPUT ReturnValue). RUN CloseProcessWindows IN hpExtra (PID, OUTPUT ReturnValue). /* ReturnValue=0 if the PID didn't own any windows. The windows may be too busy to close immediately. Give them 5 seconds to respond. That's what the Windows Task Manager would also do. */ IF ReturnValue NE 0 THEN RUN WaitForSingleObject IN hpApi (ProcessHandle, 5000, /* five seconds maximum */ OUTPUT ReturnValue). RUN CloseHandle IN hpApi(ProcessHandle, OUTPUT ReturnValue). /* third step: */ /* ----------- */ /* If PID is a Progress session it would be nice to execute PROSHUT. You would first have to find the user number via the VST _Connect table. And you would have to repeat this for every database the process is connected to. */ /* I am not going to do this, but it would have been nice... */ /* last step: */ /* ---------- */ /* because everything else failed: TerminateProcess. This is similar to "kill -9" in Unix so should be avoided */ /* Must assume we have sufficient rights for terminating this process. */ IF NOT IsProcessRunning(PID) THEN RETURN. pHow='kill'. RUN OpenProcess IN hpApi({&PROCESS_TERMINATE}, 0, PID, OUTPUT ProcessHandle). IF ProcessHandle NE 0 THEN DO: RUN TerminateProcess IN hpApi(ProcessHandle, 0, OUTPUT ReturnValue). RUN CloseHandle IN hpApi(ProcessHandle, OUTPUT ReturnValue). END. /* if everything failed the process will keep running. How could this happen? */ IF IsProcessRunning(PID) THEN pHow='failed'. END PROCEDURE.
To terminate a process for which you know the process handle, you can use function TerminateProcess.
If you don't know the process handle but the process identifier, you can get the handle by calling OpenProcess first.
DEFINE INPUT PARAMETER ProcessId AS INTEGER NO-UNDO. DEFINE VARIABLE ProcessHandle AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. RUN OpenProcess ({&PROCESS_TERMINATE}, 0, ProcessId, OUTPUT ProcessHandle). IF ProcessHandle NE 0 THEN DO: RUN TerminateProcess (ProcessHandle, 0, OUTPUT ReturnValue). RUN CloseHandle(ProcessHandle, OUTPUT ReturnValue). END.
Definitions used in this procedure:
&GLOB PROCESS_TERMINATE 1 PROCEDURE OpenProcess EXTERNAL "kernel32" : DEFINE INPUT PARAMETER dwDesiredAccess AS LONG. DEFINE INPUT PARAMETER bInheritHandle AS LONG. DEFINE INPUT PARAMETER dwProcessId AS LONG. DEFINE RETURN PARAMETER hProcess AS LONG. END PROCEDURE. PROCEDURE CloseHandle EXTERNAL "kernel32" : DEFINE INPUT PARAMETER hObject AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE TerminateProcess EXTERNAL "kernel32" : DEFINE INPUT PARAMETER hProcess AS LONG. DEFINE INPUT PARAMETER uExitCode AS LONG. DEFINE RETURN PARAMETER retval AS LONG. END PROCEDURE.
TerminateProcess is guaranteed to free all resources allocated by the process.
But, similar to "kill -9" in Unix, the process will not get the opportunity to perform any of its shutdown code. Examples of shutdown code can be: writing "recent actions" in registry, notifying other processes, saving data etc.
Because of this, TerminateProcess should only be used as an emergency measure.
A more cautious way to terminate a process would be to find all its top-level windows and send a WM_CLOSE message to each of those windows. If this does not succeed within some time interval ("not responding") you can still use TerminateProcess.
An example of how to do this is on page terminate a process gently
by Sturla Johnsen
This procedure is convenient for tech support: it shows some information about the currently running Progress process like path and name of the Progress executable ("D:\DLC\BIN\PROWIN32.EXE"), the Progress version ("8.2C") and the serial number (believe me).
DEFINE VARIABLE hModule AS INTEGER NO-UNDO. DEFINE VARIABLE cFileName AS CHARACTER NO-UNDO. DEFINE VARIABLE RetVal AS INTEGER NO-UNDO. ASSIGN hModule = ? cFileName = FILL(" ",256). RUN GetModuleFileNameA(hModule, OUTPUT cFileName, 256, OUTPUT RetVal). MESSAGE "Progress exe :" SUBSTRING(cFileName, 1, RetVal) SKIP "version:" PROVERSION SKIP "Serial number:" _SERIAL VIEW-AS ALERT-BOX.
Definitions used in this procedure, not listed in windows.p :
PROCEDURE GetModuleFileNameA EXTERNAL "kernel32.dll" : DEFINE INPUT PARAMETER hModule AS LONG. DEFINE OUTPUT PARAMETER lpFilename AS CHARACTER. DEFINE INPUT PARAMETER nSize AS LONG. DEFINE RETURN PARAMETER ReturnSize AS LONG. END PROCEDURE.
It is not required to find the actual module handle, because GetModuleFileName with module=NULL (or =? as we say in Progress) is documented to return the name of the module that started the process.
This makes it a light and convenient alternative for the source in example Modules in the current process which enumerates the names of all the modules in the current process.
An other advantage of this example is that function GetModuleFileName is available in every Windows version.
** note: this topic is outdated, needs to be adjusted for ME and XP **
The API is not exactly the same for the different Windows versions so it is sometimes usefull to know which Windows version is running. However the differences may disappear when Windows 95/98 and Windows NT mature (or when add-ons are installed) so checking for the Windows version may become less interesting: you should prefer to check for features instead versions.
This procedure here shows what Windows version you are running providing it's a 32-bit version. These are:
* Windows 3.1 with win32s
* Windows 95
* Windows 95 OSR 2
* Windows 98
* NT 3.51
* NT 4.0
* Windows 2000
* Windows CE also runs a subset of WIN32 but CE isn't interesting for us.
The procedure also shows buildnumber and CSDversion. What a CSDversion is, is not always clear: on NT it's a string describing the latest installed Service Pack. On 95 it can be anything but CSDversion will be "a" if Service Pack 1 is installed.
{windows.i} DEFINE VARIABLE lpVersionInfo AS MEMPTR. DEFINE VARIABLE dwPlatformID AS INTEGER NO-UNDO. DEFINE VARIABLE chPlatformID AS CHARACTER NO-UNDO. DEFINE VARIABLE BuildNumber AS INTEGER NO-UNDO. DEFINE VARIABLE MajorVersion AS INTEGER NO-UNDO. DEFINE VARIABLE MinorVersion AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpVersionInfo) = 148. PUT-LONG(lpVersionInfo,1) = 148. RUN GetVersionExA IN hpApi( GET-POINTER-VALUE(lpVersionInfo), OUTPUT ReturnValue). dwPlatformID = GET-LONG(lpVersionInfo,17). CASE dwPlatformID : WHEN 0 THEN chPlatformID = "Win32s on Windows 3.1". WHEN 1 THEN chPlatformID = "Win32 on Windows 95 or 98". WHEN 2 THEN chPlatformID = "Win32 on Windows NT". END. CASE dwPlatformID : WHEN 1 THEN BuildNumber = GET-SHORT(lpVersionInfo,13). WHEN 2 THEN BuildNumber = GET-LONG (lpVersionInfo,13). /* what about 'when 0' for 3.1 with win32s ?? */ END. /* You have Windows 95 OSR 2 if: dwPlatformID=1 and LOWORD(BuildNumber)=1111 (probably hex??) Unfortunately I have not had a chance to test that. */ CASE dwPlatformID : WHEN 1 THEN DO: MinorVersion = GET-BYTE(lpVersionInfo,15). MajorVersion = GET-BYTE(lpVersionInfo,16). END. OTHERWISE DO: MajorVersion = GET-LONG(lpVersionInfo, 5). MinorVersion = GET-LONG(lpVersionInfo, 9). END. END. MESSAGE "MajorVersion=" MajorVersion SKIP "MinorVersion=" MinorVersion SKIP "BuildNumber=" BuildNumber SKIP "PlatformID=" chPlatFormId SKIP "CSDversion=" GET-STRING(lpVersionInfo,21) SKIP(2) "on NT, CSDversion contains version of latest Service Pack" SKIP "on 95/98, CSDversion contains arbitrary extra info, if any" VIEW-AS ALERT-BOX. SET-SIZE(lpVersionInfo) = 0.
To check if you are running on Terminal Server Edition you can use function ValidateProductSuite("Terminal Server").
Old documentation suggested that this function would be added to the WIN32 API in Windows 2000. But newer documentation for Windows 2000 describes a new function VerifyVersionInfo - to be called with wSuiteMask = VER_SUITE_TERMINAL. We will see.
In the meantime you can write your own function ValidateProductSuite in Progress 4GL and some registry functions. An example is on page Disallowing multiple instances of your application.
FUNCTION RunningWindows95 RETURNS LOGICAL () : /* returns TRUE if you are running Windows 95 */ DEFINE VARIABLE Win95 AS LOGICAL NO-UNDO. DEFINE VARIABLE lpVersionInfo AS MEMPTR. DEFINE VARIABLE dwPlatformID AS INTEGER NO-UNDO. DEFINE VARIABLE MinorVersion AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpVersionInfo) = 148. PUT-LONG(lpVersionInfo,1) = 148. RUN GetVersionExA IN hpApi( GET-POINTER-VALUE(lpVersionInfo), OUTPUT ReturnValue). dwPlatformID = GET-LONG(lpVersionInfo,17). MinorVersion = GET-BYTE(lpVersionInfo,15). Win95 = (dwPlatformId=1 AND MinorVersion=0). SET-SIZE(lpVersionInfo) = 0. RETURN Win95. END FUNCTION. FUNCTION RunningWindows98 RETURNS LOGICAL () : /* returns TRUE if you are running Windows 98 */ DEFINE VARIABLE Win98 AS LOGICAL NO-UNDO. DEFINE VARIABLE lpVersionInfo AS MEMPTR. DEFINE VARIABLE dwPlatformID AS INTEGER NO-UNDO. DEFINE VARIABLE MinorVersion AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpVersionInfo) = 148. PUT-LONG(lpVersionInfo,1) = 148. RUN GetVersionExA IN hpApi( GET-POINTER-VALUE(lpVersionInfo), OUTPUT ReturnValue). dwPlatformID = GET-LONG(lpVersionInfo,17). MinorVersion = GET-BYTE(lpVersionInfo,15). Win98 = (dwPlatformId=1 AND MinorVersion=10). SET-SIZE(lpVersionInfo) = 0. RETURN Win98. END FUNCTION. FUNCTION RunningWindowsNT4 RETURNS LOGICAL () : /* returns TRUE if you are running Windows NT4. I have not had a chance to test this yet */ DEFINE VARIABLE NT4 AS LOGICAL NO-UNDO. DEFINE VARIABLE lpVersionInfo AS MEMPTR. DEFINE VARIABLE dwPlatformID AS INTEGER NO-UNDO. DEFINE VARIABLE MajorVersion AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpVersionInfo) = 148. PUT-LONG(lpVersionInfo,1) = 148. RUN GetVersionExA IN hpApi( GET-POINTER-VALUE(lpVersionInfo), OUTPUT ReturnValue). dwPlatformID = GET-LONG(lpVersionInfo,17). MajorVersion = GET-BYTE(lpVersionInfo, 5). NT4 = (dwPlatformId=2 AND MajorVersion=4). SET-SIZE(lpVersionInfo) = 0. RETURN NT4. END FUNCTION. FUNCTION RunningWindows2000 RETURNS LOGICAL () : /* returns TRUE if you are running Windows 2000 */ DEFINE VARIABLE Win2000 AS LOGICAL NO-UNDO. DEFINE VARIABLE lpVersionInfo AS MEMPTR. DEFINE VARIABLE dwPlatformID AS INTEGER NO-UNDO. DEFINE VARIABLE MajorVersion AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpVersionInfo) = 148. PUT-LONG(lpVersionInfo,1) = 148. RUN GetVersionExA IN hpApi( GET-POINTER-VALUE(lpVersionInfo), OUTPUT ReturnValue). dwPlatformID = GET-LONG(lpVersionInfo,17). MajorVersion = GET-BYTE(lpVersionInfo, 5). Win2000 = (dwPlatformId=2 AND MajorVersion=5). SET-SIZE(lpVersionInfo) = 0. RETURN Win2000. END FUNCTION.
Brad Long added this procedure which is indeed convenient.
FUNCTION WINGetVersion RETURNS CHARACTER () : /*----------------------------------------------------------------------------- Purpose: Calls the WINAPI function GetVersionExA to determine the version of the Windows operating system that is running on the machine. Notes: Returns "95" for Windows 95, "98" for Windows 98, "NT" for Windows NT Returns "undef" if unable to determine platform. ------------------------------------------------------------------------------*/ DEFINE VARIABLE v_version-buf AS MEMPTR. DEFINE VARIABLE v_platform-id AS INTEGER NO-UNDO. DEFINE VARIABLE v_platform-desc AS CHARACTER NO-UNDO. DEFINE VARIABLE v_major-version AS INTEGER NO-UNDO. DEFINE VARIABLE v_minor-version AS INTEGER NO-UNDO. DEFINE VARIABLE v_return-value AS INTEGER NO-UNDO. SET-SIZE(v_version-buf) = 148. PUT-LONG(v_version-buf,1) = 148. RUN GetVersionExA (INPUT GET-POINTER-VALUE(v_version-buf), OUTPUT v_return-value). v_platform-id = GET-LONG(v_version-buf,17). CASE v_platform-id: WHEN 1 THEN DO: v_minor-version = GET-BYTE(v_version-buf,15). v_major-version = GET-BYTE(v_version-buf,16). END. OTHERWISE DO: v_major-version = GET-LONG(v_version-buf,5). v_minor-version = GET-LONG(v_version-buf,9). END. END. CASE v_platform-id: WHEN 0 THEN v_platform-desc = "3.1". WHEN 1 THEN DO: IF v_minor-version EQ 0 THEN v_platform-desc = "95". ELSE IF v_minor-version GT 0 THEN v_platform-desc = "98". ELSE v_platform-desc = "undef". END. WHEN 2 THEN v_platform-desc = "NT". OTHERWISE v_platform-desc = "undef". END. SET-SIZE(v_version-buf) = 0. RETURN v_platform-desc. END FUNCTION.
There are P4GL functions for setting and getting registry information. I am not familiar with those P4GL functions so it may be silly to use Windows functions instead.
One thing the P4GL can not do is accessing (hexa)decimal data.
Before you try to access the registry directly you should try to find some specialized API call for the requested information type.
For example:
You can read a list of available printers from the registry, but you can also use the EnumPrinters API function for this task. It is generally speaking better to use the API function, because these are documented while the exact registry-locations are not. The EnumPrinters example is only one example. I don't know, but I guess there are documented functions for everything in the registry.
The point is, I think you should try to find an API function before you start digging in the registry directly. Unless of course you are writing and reading in your own key.
Suppose there is a registry key
HKEY_LOCAL_MACHINE\Software\Nerdware\Test
and suppose it contains a DWORD value, named "count".
This source example will read the value of "count", identifies the datatype, and increment the value of "count" by one.
{windows.i} /* March 28, 1998 or later */ DEFINE VARIABLE key-hdl AS INTEGER NO-UNDO. DEFINE VARIABLE lpBuffer AS MEMPTR NO-UNDO. DEFINE VARIABLE lth AS INTEGER NO-UNDO. DEFINE VARIABLE reslt AS INTEGER NO-UNDO. DEFINE VARIABLE datatype AS INTEGER NO-UNDO. DEFINE VARIABLE icount AS INTEGER NO-UNDO. RUN RegOpenKeyA IN hpApi ( {&HKEY_LOCAL_MACHINE}, "Software\Nerdware\Test", OUTPUT key-hdl, OUTPUT reslt). IF reslt NE {&ERROR_SUCCESS} THEN DO: MESSAGE "key not found in registry" VIEW-AS ALERT-BOX. RETURN. END. /* read value of "count" into lpBuffer */ /* make buffer large, because we don't know for sure if it is really a DWORD. The maximum size is supposed to be MAX_PATH + 1 */ ASSIGN lth = {&MAX_PATH} + 1 SET-SIZE(lpBuffer) = lth. RUN RegQueryValueExA IN hpApi (key-hdl, "count", 0, /* reserved, must be 0 */ OUTPUT datatype, GET-POINTER-VALUE(lpBuffer), INPUT-OUTPUT lth, OUTPUT reslt). IF reslt NE {&ERROR_SUCCESS} THEN MESSAGE "value not found in registry" VIEW-AS ALERT-BOX. ELSE DO: CASE datatype : WHEN 1 THEN MESSAGE "datatype=STRING" SKIP "value=" GET-STRING(lpBuffer,1) VIEW-AS ALERT-BOX. WHEN 4 THEN MESSAGE "datatype=DWORD" SKIP "value=" GET-LONG(lpBuffer,1) VIEW-AS ALERT-BOX. OTHERWISE MESSAGE "unexpected datatype:" SKIP "datatype=" datatype SKIP VIEW-AS ALERT-BOX. END CASE. /* if it is a DWORD, then increment the value */ IF datatype=4 THEN DO: icount = GET-LONG(lpBuffer,1). SET-SIZE(lpBuffer) = 4. /* =sizeof(DWORD) */ PUT-LONG(lpBuffer,1) = icount + 1. RUN RegSetValueExA IN hpApi (key-hdl, "count", INPUT 0, /* reserved, must be 0 */ INPUT 4, /* = REG_DWORD */ INPUT GET-POINTER-VALUE(lpBuffer), INPUT 4, /* = get-size(lpBuffer) */ OUTPUT reslt). IF reslt NE {&ERROR_SUCCESS} THEN MESSAGE "can not write value in registry" VIEW-AS ALERT-BOX. END. END. SET-SIZE(lpBuffer)=0. RUN RegCloseKey IN hpApi (key-hdl,OUTPUT reslt).
function RegSetValueEx will create the value "count" if it did not exist yet. So even if RegQueryValue did not find the value, you can still set one. Of course you will have to change the logic of this source example to allow that.
There are more datatypes than just REG_SZ (=1) and REG_DWORD (=4). Check your API documentation for a complete list.
This example enumerates all values in a given registry key.
In this case, it will return a list of all installed printers (with their names and ports), so it actually does the same as the EnumPrinters function.
As explained in the registry introduction you should prefer to use the EnumPrinters function instead of this RegEnumA stuff, because Microsoft may decide to store the printers info elsewhere in the future, while the EnumPrinters API will be maintained.
{windows.i} /* March 28, 1998 or later */ DEFINE VARIABLE hKey AS INTEGER NO-UNDO. DEFINE VARIABLE hPrinterkey AS INTEGER NO-UNDO. DEFINE VARIABLE subkey AS CHARACTER NO-UNDO. DEFINE VARIABLE port AS MEMPTR NO-UNDO. DEFINE VARIABLE lth AS INTEGER NO-UNDO. DEFINE VARIABLE reslt AS INTEGER NO-UNDO. DEFINE VARIABLE datatype AS INTEGER NO-UNDO. DEFINE VARIABLE ITEM AS INTEGER NO-UNDO. RUN RegOpenKeyA IN hpApi( {&HKEY_LOCAL_MACHINE}, "System\CurrentControlSet\control\Print\Printers", OUTPUT hKey, OUTPUT reslt). ASSIGN ITEM = 0 reslt = 0. DO WHILE reslt NE {&ERROR_NO_MORE_ITEMS} : ASSIGN lth = {&MAX_PATH} + 1 subkey = FILL("x", lth). RUN RegEnumKeyA IN hpApi (hKey, ITEM, OUTPUT subkey, INPUT LENGTH(subkey), OUTPUT reslt). IF reslt NE {&ERROR_NO_MORE_ITEMS} THEN DO: /* get the printer port (or description..) */ RUN RegOpenKeyA IN hpApi ( hKey, subkey, OUTPUT hPrinterkey, OUTPUT reslt). lth = {&MAX_PATH} + 1. SET-SIZE(port) = lth. RUN RegQueryValueExA IN hpApi (hPrinterkey, "port", 0, /* reserved, must be 0 */ OUTPUT datatype, GET-POINTER-VALUE(port), INPUT-OUTPUT lth, OUTPUT reslt). RUN RegCloseKey IN hpApi (hPrinterkey,OUTPUT reslt). MESSAGE "printer name=" subkey SKIP "port=" GET-STRING(port,1) VIEW-AS ALERT-BOX. END. ITEM = ITEM + 1. END. /* do while not ERROR_NO_MORE_ITEMS */ SET-SIZE(port)=0. RUN RegCloseKey IN hpApi (hKey,OUTPUT reslt).
Obviously a (sub)key may contain values with different datatypes, like strings and numbers. You should test the 'datatype' parameter before you interpret a value. For an example of this, see RegQueryValueEx
Function RegCreateKeyEx creates a registry key and opens it. If the key already exists, the function will just open it.
{windows.i} &SCOPED-DEFINE KEY_ALL_ACCESS 983103 &SCOPED-DEFINE REG_OPTION_NON_VOLATILE 0 &SCOPED-DEFINE REG_OPTION_VOLATILE 1 &SCOPED-DEFINE REG_CREATED_NEW_KEY 1 &SCOPED-DEFINE REG_OPENED_EXISTING_KEY 2 PROCEDURE RegCreateKeyEx{&A} EXTERNAL {&ADVAPI} : DEFINE INPUT PARAMETER hkey AS LONG. DEFINE INPUT PARAMETER lpszSubKey AS CHARACTER. DEFINE INPUT PARAMETER dwReserved AS LONG. DEFINE INPUT PARAMETER plszClass AS CHARACTER. DEFINE INPUT PARAMETER dwOptions AS LONG. DEFINE INPUT PARAMETER samDesired AS LONG. DEFINE INPUT PARAMETER lpSecurityAttributes AS LONG. DEFINE OUTPUT PARAMETER phkResult AS LONG. DEFINE OUTPUT PARAMETER lpdwDisposition AS LONG. DEFINE RETURN PARAMETER lpResult AS LONG. END PROCEDURE. /* example : */ DEFINE VARIABLE key-hdl AS INTEGER NO-UNDO. DEFINE VARIABLE dwDisposition AS INTEGER NO-UNDO. DEFINE VARIABLE reslt AS INTEGER NO-UNDO. RUN RegCreateKeyEx{&A} IN /* hpApi */ THIS-PROCEDURE ( {&HKEY_CLASSES_ROOT}, "SoftWare\MyCompany", 0, '':U, {®_OPTION_NON_VOLATILE}, {&KEY_ALL_ACCESS}, 0, OUTPUT key-hdl, OUTPUT dwDisposition, OUTPUT reslt). IF reslt NE 0 THEN MESSAGE "function RegCreateKeyEx failed" VIEW-AS ALERT-BOX. ELSE CASE dwDisposition : WHEN {®_CREATED_NEW_KEY} THEN MESSAGE "created new key" VIEW-AS ALERT-BOX. WHEN {®_OPENED_EXISTING_KEY} THEN MESSAGE "opened existing key" VIEW-AS ALERT-BOX. END. /* use the key */ .... /* close it */ RUN RegCloseKey IN hpApi (key-hdl, OUTPUT reslt).
The example creates key "Software\MyCompany" or opens it if the key already exists.
Variable key-hdl returns a handle to the open key. This handle can be used in subsequent functions and must eventually be closed by function RegCloseKey.
.
Chris Herring submitted the following code to draw the associated icon for any document on a button. For example: you created a button to open a file like "c:\invoices\abc001.doc", the file extension "doc" is associated with MS-Word so you want the icon for Word-documents displayed on the Progress button.
The remainder of the text is from Chris. As you see, he has some issues left. If you know the solution, please click the "Edit"-link near the bottom of this page to help out.
There were two ways I found to add the icon, using SendMessageA and ImageList_Draw. SendMessageA works ok except it suffers from the same limitations and the toggle button - can't remove focus. ImageList_Draw seems to be able to draw the icon on progress widgets that have the HWND attribute( at least the button and frame anyway ) but the image is release whenever the display for the image is refreshed. On the plus side it would work with flat buttons if you could get the image to stay. Anyway I've put some of my code below. Only thing that I'm aware of that is not entirely correct is the size of the fileinfo memptr regarding the last two string parameters. I wasn't sure of the size so I set them to 1 which only returns the first char of the string if the display name and type tags are added to the flag.
&GLOBAL-DEFINE GWL_STYLE -16 &GLOBAL-DEFINE BM_SETIMAGE 247 &GLOBAL-DEFINE IMAGE_ICON 1 &GLOBAL-DEFINE SHGFI_ICON 256 &GLOBAL-DEFINE SHGFI_SYSICONINDEX 16384 &GLOBAL-DEFINE SHGFI_USEFILEATTRIBUTES 16 &GLOBAL-DEFINE FILE_ATTRIBUTE_NORMAL 128 &GLOBAL-DEFINE BS_ICON 64 PROCEDURE SHGetFileInfo EXTERNAL 'shell32': DEFINE INPUT PARAMETER pszPath AS CHARACTER. DEFINE INPUT PARAMETER dwFileAttributes AS LONG. DEFINE OUTPUT PARAMETER SBFileInfo AS MEMPTR. DEFINE INPUT PARAMETER cbSizeFileInfo AS LONG. DEFINE INPUT PARAMETER uFlags AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE ImageList_Draw EXTERNAL "comctl32": DEFINE INPUT PARAMETER himl AS LONG. DEFINE INPUT PARAMETER iIndex AS LONG. DEFINE INPUT PARAMETER hdc AS LONG. DEFINE INPUT PARAMETER X AS LONG. DEFINE INPUT PARAMETER Y AS LONG. DEFINE INPUT PARAMETER iStyle AS LONG. DEFINE RETURN PARAMETER lBool AS LONG. END PROCEDURE. PROCEDURE setButton : /*-- Purpose: gets the icon from a program, file, or extension and draws the icon on the specified button Notes: the button must not be flat or no-focus ------------------------------------------------------------------------------*/ DEFINE INPUT PARAMETER iHwnd AS INTEGER NO-UNDO. DEFINE INPUT PARAMETER iphButton AS HANDLE NO-UNDO. DEFINE INPUT PARAMETER pcProgram AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER plExtension AS LOGICAL NO-UNDO. DEFINE VARIABLE iSize AS INTEGER NO-UNDO. DEFINE VARIABLE fileinfo AS MEMPTR NO-UNDO. DEFINE VARIABLE iFlags AS INTEGER NO-UNDO. DEFINE VARIABLE iReturn AS INTEGER NO-UNDO. DEFINE VARIABLE hIcon AS INTEGER NO-UNDO. DEFINE VARIABLE iIcon AS INTEGER NO-UNDO. DEFINE VARIABLE iAttr AS INTEGER NO-UNDO. DEFINE VARIABLE cDName AS CHARACTER NO-UNDO. DEFINE VARIABLE cType AS CHARACTER NO-UNDO. DEFINE VARIABLE iStyles AS INTEGER NO-UNDO. DEFINE VARIABLE iOldIcon AS INTEGER NO-UNDO. SET-SIZE(fileinfo) = 4 + 4 + 4 + 1 + 1. iSize = GET-SIZE(fileinfo). IF iSize NE 0 THEN DO: /*RUN SHGetFileInfo( "C:\Program Files\Internet Explorer\IEXPLORE.EXE", 0, OUTPUT fileinfo, iSize, iFlags, OUTPUT lReturn ).*/ /*RUN SHGetFileInfo( ".html", 128, OUTPUT fileinfo, iSize, iFlags, OUTPUT lReturn ).*/ IF plExtension THEN DO: iFlags = {&SHGFI_ICON} + {&SHGFI_USEFILEATTRIBUTES}. RUN SHGetFileInfo( pcProgram, {&FILE_ATTRIBUTE_NORMAL}, OUTPUT fileinfo, iSize, iFlags, OUTPUT iReturn ). END. ELSE DO: iFlags = {&SHGFI_ICON}. RUN SHGetFileInfo( pcProgram, 0, OUTPUT fileinfo, iSize, iFlags, OUTPUT iReturn ). END. hIcon = GET-LONG(fileinfo, 1). iIcon = GET-LONG(fileinfo, 5). iAttr = GET-LONG(fileinfo, 9). cDName = GET-STRING(fileinfo, 13). cType = GET-STRING(fileinfo, 14). SET-SIZE(fileinfo) = 0. END. RUN GetWindowLongA(iHwnd, {&GWL_STYLE}, OUTPUT iStyles). IF iphButton:PRIVATE-DATA EQ ? THEN ASSIGN iStyles = iStyles + {&BS_ICON} iphButton:PRIVATE-DATA = "loaded". ELSE ASSIGN iStyles = iStyles. RUN SetWindowLongA(iHwnd, {&GWL_STYLE}, iStyles, OUTPUT iStyles). RUN SendMessageA( iHwnd, {&BM_SETIMAGE}, {&IMAGE_ICON}, hIcon, OUTPUT iOldIcon). END PROCEDURE.
This example has some advantages over the older example. This example shows an easy way to create buttons that can have a "pushed" state. These buttons are actually toggle-box widgets with an alternative layout. That is convenient because you don't need any special code to read and set the logical value: the usual statement toggle-1=TRUE will be sufficient to show the button in pushed state.
Width and height are set high enough to fit the image. You can simply use the UIB/AB to do this.
You don't really need to put images on these buttons: just don't use the BS_ICON style (or BS_BITMAP) if you want to keep the label.
Note: I could not manage to get a NO-FOCUS effect. If you have ideas about this please let me know.
RUN ToggleToButton (toggle-1:HWND). /* show an icon or bitmap on the button. There are several ways of doing this. I got lazy so I chose this method for 32x32 icons. */ RUN SetIcon (toggle-1:HWND, 'winupd.ico').
PROCEDURE ToggleToButton : /* ------------------------------------------------------------------- purpose: convert a toggle-box widget to a button. note : don't call this more than once for each toggle-box widget ------------------------------------------------------------------- */ DEFINE INPUT PARAMETER HWND AS INTEGER. DEFINE VARIABLE styles AS INTEGER NO-UNDO. DEFINE VARIABLE returnvalue AS INTEGER NO-UNDO. /* find the current style and add some extra flags to it */ RUN GetWindowLongA(HWND, {&GWL_STYLE}, OUTPUT styles). styles = styles + {&BS_ICON} + {&BS_PUSHLIKE}. /* according to MSDN you should apply the new style using SendMessage(hwnd,BM_SETSTYLE,....) but it does not work for me */ RUN SetWindowLongA(HWND, {&GWL_STYLE}, styles, OUTPUT styles). /* force a repaint: */ RUN InvalidateRect(HWND,0,1,OUTPUT returnvalue). END PROCEDURE.
PROCEDURE SetIcon : DEFINE INPUT PARAMETER HWND AS INTEGER. DEFINE INPUT PARAMETER IconFilename AS CHARACTER. DEFINE VARIABLE hInstance AS INTEGER NO-UNDO. DEFINE VARIABLE OldIcon AS INTEGER NO-UNDO. DEFINE VARIABLE hIcon AS INTEGER NO-UNDO. RUN GetWindowLongA(HWND, {&GWL_HINSTANCE}, OUTPUT hInstance). RUN ExtractIconA (hInstance, IconFilename, 0, OUTPUT hIcon). RUN SendMessageA( HWND, {&BM_SETIMAGE}, {&IMAGE_ICON}, hIcon, OUTPUT OldIcon). /* free resources when the window closes, or earlier: run DestroyIcon (hIcon). */ IF OldIcon NE 0 THEN RUN DestroyIcon (OldIcon). END PROCEDURE.
Definitions used in this example:
&GLOBAL-DEFINE GWL_HINSTANCE -6 &GLOBAL-DEFINE GWL_STYLE -16 &GLOBAL-DEFINE BS_PUSHLIKE 4096 &GLOBAL-DEFINE BS_ICON 64 &GLOBAL-DEFINE BS_BITMAP 128 &GLOBAL-DEFINE BM_SETIMAGE 247 &GLOBAL-DEFINE IMAGE_ICON 1 &GLOBAL-DEFINE IMAGE_BITMAP 0 &GLOBAL-DEFINE BM_SETSTYLE 244 PROCEDURE ExtractIconA EXTERNAL "shell32.dll" : DEFINE INPUT PARAMETER hInst AS LONG. DEFINE INPUT PARAMETER lpszExeFileName AS CHARACTER. DEFINE INPUT PARAMETER nIconIndex AS LONG. DEFINE RETURN PARAMETER hIcon AS LONG. END PROCEDURE. PROCEDURE DestroyIcon EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER hIcon AS LONG. END PROCEDURE. PROCEDURE GetWindowLongA EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER phwnd AS LONG. DEFINE INPUT PARAMETER cindex AS LONG. DEFINE RETURN PARAMETER currentlong AS LONG. END PROCEDURE. PROCEDURE SetWindowLongA EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER phwnd AS LONG. DEFINE INPUT PARAMETER cindex AS LONG. DEFINE INPUT PARAMETER newlong AS LONG. DEFINE RETURN PARAMETER oldlong AS LONG. END PROCEDURE. PROCEDURE InvalidateRect EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER lpRect AS LONG. DEFINE INPUT PARAMETER bErase AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE SendMessageA EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER umsg AS LONG. DEFINE INPUT PARAMETER wparam AS LONG. DEFINE INPUT PARAMETER lparam AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.
See for an improved version: Buttons in pushed state
This example works best with Progress version 9.
When you create a 'toolbar' with buttons, you may sometimes want to keep a button in a 'pushed' state indicating if the functionality for the button is toggled 'on'.
In that case you could run the following ToggleButton procedure during the ON CHOOSE event of the button widget:
{windows.i} PROCEDURE ToggleButton : DEFINE INPUT PARAMETER hbutton AS HANDLE. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. &SCOP BM_SETSTATE 243 IF hbutton:TYPE NE 'button' THEN RETURN. IF hbutton:PRIVATE-DATA = 'pushed' THEN DO: hbutton:PRIVATE-DATA = ''. RUN SendMessageA IN hpApi ( hbutton:HWND, {&BM_SETSTATE}, 0, /* FALSE, remove BST_PUSHED style */ 0, OUTPUT ReturnValue). END. ELSE DO: hbutton:PRIVATE-DATA = 'pushed'. RUN SendMessageA IN hpApi ( hbutton:HWND, {&BM_SETSTATE}, 1, /* TRUE, set BST_PUSHED style */ 0, OUTPUT ReturnValue). END. END PROCEDURE.
There are several button types in Progress: flat, no-focus, buttons with images and 'normal' buttons with text. In version 9 this procedure works for all button types, but in version 8 it works only for buttons with no images.
This enhancement is undocumented.
I have used the private-data attribute to store the current state of the button. It would also be possible to use the BM_GETSTATE message to query if the button is in it's 'pushed' state, but this is not useful during the ON CHOOSE event. After all, the button is always pushed during the ON CHOOSE event!
There is a however a problem: when focus moves away to another (non-Progress) application, all pushed buttons become 'unpushed'. This problem is reported to PSC and may be fixed in the future, but unfortunately it has a very low priority.
example by Simon de Kraa
This example program replaces the rectangular region of a window by a polygon. The technique is basically the same as described on page round widgets. You can download the example source from the attachments table.
A polygon is an array of points. The coordinates for the points in this example are calculated using the trigonometry functions sin and cos (found in the Visual C runtime module which is available on almost every PC). The number of points can be selected using the slider control; selecting a large number of points is somewhat slow but gives an interesting result as shown in the picture.
API declarations used in this example:
PROCEDURE CreatePolygonRgn EXTERNAL "gdi32.dll": DEFINE INPUT PARAMETER lpPoint AS MEMPTR. DEFINE INPUT PARAMETER nCount AS LONG. DEFINE INPUT PARAMETER nPolyFillMode AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE sin EXTERNAL "MSVCRT40.DLL" CDECL: DEFINE INPUT PARAMETER dblValue AS DOUBLE NO-UNDO. DEFINE RETURN PARAMETER dblResult AS DOUBLE NO-UNDO. END PROCEDURE. PROCEDURE cos EXTERNAL "MSVCRT40.DLL" CDECL: DEFINE INPUT PARAMETER dblValue AS DOUBLE NO-UNDO. DEFINE RETURN PARAMETER dblResult AS DOUBLE NO-UNDO. END PROCEDURE.
polygon.zip : demo source
Page Selection-list with item-data shows how and when to use itemdata with a selection list. The same works for a combo-box, except the message constants are different.
Let's skip the background info and go right to the source:
{windows.i} PROCEDURE AddComboItem : /*------------------------------------------------------------------------------ Purpose: add an item to the combo-box. Each item can be associated with itemdata (is low-level implementation of PRIVATE-DATA). ------------------------------------------------------------------------------*/ DEFINE INPUT PARAMETER ip_hCombobox AS HANDLE NO-UNDO. DEFINE INPUT PARAMETER ip_ItemText AS CHARACTER NO-UNDO. DEFINE INPUT PARAMETER ip_ItemData AS INTEGER NO-UNDO. DEFINE VARIABLE lpString AS MEMPTR NO-UNDO. DEFINE VARIABLE SelIndex AS INTEGER NO-UNDO. DEFINE VARIABLE retval AS INTEGER NO-UNDO. SET-SIZE(lpString) = 0. SET-SIZE(lpString) = LENGTH(ip_ItemText) + 1. PUT-STRING(lpString,1) = ip_ItemText. RUN SendMessageA IN hpAPi (ip_hCombobox:HWND, 323, /* = CB_ADSTRING */ 0, GET-POINTER-VALUE(lpString), OUTPUT selIndex). RUN SendMessageA IN hpApi (ip_hCombobox:HWND IN FRAME frame-tools , 337, /* = CB_SETITEMDATA */ SelIndex, ip_ItemData, OUTPUT retval). SET-SIZE(lpString)=0. RETURN. END PROCEDURE.
The first call to SendMessage adds a text to the combo-box, so it does the same as combo-1:ADD-LAST(ip_ItemText). However it also returns a SelIndex, which is the unique and constant index number for the new combo item. This SelIndex is used in the second call to SendMessage.
The second call to SendMessage adds an integer data value to the combo-item.
{windows.i} ON VALUE-CHANGED OF COMBO-1 IN FRAME FRAME-tools /* Procedure */ DO: DEFINE VARIABLE SelIndex AS INTEGER NO-UNDO. DEFINE VARIABLE ItemData AS INTEGER NO-UNDO. RUN SendMessageA (combo-1:HWND, 327, /* = CB_GETCURSEL */ 0, 0, OUTPUT SelIndex). RUN SendMessageA (combo-1:HWND, 336, /* = CB_GETITEMDATA */ SelIndex, 0, OUTPUT ItemData). RUN Whatever(ItemData). END.
Normally I would prefer to use the Progress 9 feature of ITEM-DATA-PAIRS instead of this API solution. But ITEM-DATA-PAIRS are designed to work only if the screen-values are unique!
A typical Windows application has an Edit menu with options for Undo, Cut, Copy and Paste. There are 4GL methods for making such a menu. This site usually does not cover 4GL methods, but I would like to make an exception this time because these 4GL methods are not documented. This was posted by email to PEG by Matt Gilarde:
Starting in 8.2A, Progress supports several new attributes and methods which make implementing the Edit menu very simple. Unfortunately, these features were added in the late stage of 8.2 development and were not adequately documented. Briefly, the new attributes and methods are: Editor only: EDIT-CAN-UNDO - TRUE if editor can undo last operation EDIT-UNDO() - undo last operation Fill-in and Editor: EDIT-CAN-PASTE - TRUE if the clipboard contains pastable text EDIT-PASTE() - paste clipboard text into editor or fill-in EDIT-CLEAR() - delete contents of selection in editor or fill-in EDIT-CUT() - cut selected contents of editor or fill-in to clipboard EDIT-COPY() - copy selected contents of editor or fill-in to clipboard The following methods and attributes will work for fill-ins in Skywalker (they currently work only for editors): CLEAR-SELECTION() SELECTION-END SELECTION-START SELECTION-TEXT SET-SELECTION() TEXT-SELECTED READ-ONLY Matt Gilarde - PSC Development
by Stuart Morris
Suppose you have a combo-box widget (named combo-box-1) and you want to drop down its built-in selection-list on some event. The following source will do just that.
{windows.i} &GLOBAL-DEFINE CB_SHOWDROPDOWN 335 DEFINE VARIABLE retval AS INTEGER NO-UNDO. RUN SendMessage{&A} IN hpApi (INPUT COMBO-BOX-1:HWND, {&CB_SHOWDROPDOWN}, 1, /* True */ 0, OUTPUT retval ) NO-ERROR /* Stop C Stack Errors */.
based on an example by Sturla Johnsen
The animated gif shows an impression of Rounddemo.w.
Rounddemo, written by Sturla Johnsen, creates a normal Progress dialog box with some widgets on it (a frame, two fill-ins and two buttons) and demonstrates how the default rectangular region of any widget can be replaced by a different shaped region. In this case by elliptic regions.
Rounddemo.w calls procedure Roundwidget.p from within the CHOOSE trigger of the button. Both procedures are attched (see table near end of this topic) and can be downloaded from here to see for yourself.
ON CHOOSE OF BUTTON-1 IN FRAME Dialog-Frame /* Press me (several times) */ DO: IF SELF:PRIVATE-DATA = ? THEN ASSIGN SELF:PRIVATE-DATA = "1". DEFINE VARIABLE HANDLE AS HANDLE. CASE INT(SELF:PRIVATE-DATA): WHEN 1 THEN ASSIGN HANDLE = FRAME {&FRAME-NAME}:HANDLE. WHEN 2 THEN ASSIGN HANDLE = FILL-IN-1:HANDLE. WHEN 3 THEN ASSIGN HANDLE = FILL-IN-2:HANDLE. WHEN 4 THEN ASSIGN HANDLE = FRAME FRAME-A:HANDLE. WHEN 5 THEN ASSIGN HANDLE = SELF. WHEN 6 THEN ASSIGN HANDLE = Btn_Close:HANDLE SELF:SENSITIVE = NO. OTHERWISE RETURN NO-APPLY. END CASE. RUN roundwidget.p (HANDLE). ASSIGN SELF:PRIVATE-DATA = STRING(INT(SELF:PRIVATE-DATA) + 1). END.
/******************************************* * roundwidget.p * * Sturla Johnsen * *******************************************/ DEFINE INPUT PARAMETER hWidget AS HANDLE. DEFINE VARIABLE hrgn AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. IF NOT VALID-HANDLE(hWidget) THEN RETURN ERROR. RUN CreateEllipticRgn (1, /* Start Xpos */ 1, /* Start Ypos */ hWidget:WIDTH-PIXELS, hWidget:HEIGHT-PIXELS, OUTPUT hrgn). RUN SetWindowRgn(hWidget:HWND, hrgn, 1, /* 1 = Redraw */ OUTPUT ReturnValue). PROCEDURE SetWindowRgn EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER hRgn AS LONG. DEFINE INPUT PARAMETER bRedraw AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE. PROCEDURE CreateEllipticRgn EXTERNAL "gdi32.dll" : DEFINE INPUT PARAMETER StartX AS LONG. DEFINE INPUT PARAMETER StartY AS LONG. DEFINE INPUT PARAMETER HEIGHT AS LONG. DEFINE INPUT PARAMETER WIDTH AS LONG. DEFINE RETURN PARAMETER hrgn AS LONG. END PROCEDURE.
CreateEllipticRgn creates a region which is a GDI object. A program should delete all the GDI objects it creates (using procedure DeleteObject). But not this time, because it is passed to SetWindowRgn. From now on this region is owned by the operating system and will eventually be deleted automatically.
There are many more API functions for creating regions with different shapes, like CreatePolygonRgn, CreateRectRgn, CreateRoundRectRgn. You can also create several regions and combine them using CombineRgn to create every shape you can imagine.
rounddemo.zip : Example code
Every widget can have private-data, but it is less commonly known that each list-item in a selection list can have its own private data.
The itemdata for a list-item is of type integer, or actually a DWORD.
Perhaps it is best to think of a selection list as a container of list-items, where each list-item is uniquely identified by a SelIndex. Each list-item contains two data members: the (visible) character string and the itemdata. The itemdata has no meaning to Windows so you can safely use it for storing application-specific information.
Normally I would prefer to use the Progress 9 feature of LIST-ITEM-PAIRS instead of this API solution. But LIST-ITEM-PAIRS are designed to work only if the LIST-ITEMs (e.g. screen-values) are unique!
The following example populates a selection-list widget with each customer.name. When the user chooses a selection-list item, the customer is fetched and its details are displayed. This would normally not be possible because:
* There is no index on customer.name so it would be slow to FIND CUSTOMER WHERE customer.name=screen-value
* There may be several customers with the same name, so the FIND statement may be ambiguous
The solution is to add an identifying value to each list-item. This example stores RECID(customer) in the itemdata. I know RECID is an obsolete function, but I have used it anyway in this example because RECIDs are unique and compatible with Integers.
{windows.i} PROCEDURE Populate : DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE lpString AS MEMPTR. DEFINE VARIABLE selindex AS INTEGER NO-UNDO. FOR EACH customer NO-LOCK : SET-SIZE(lpString) = 0. SET-SIZE(lpString) = LENGTH(TRIM(customer.NAME)) + 1. PUT-STRING(lpString,1) = TRIM(customer.NAME). RUN SendMessageA IN hpApi (select-1:HWND IN FRAME {&frame-name}, 384, /* LB_ADSTRING */ 0, GET-POINTER-VALUE(lpString), OUTPUT selIndex). RUN SendMessageA IN hpApi (select-1:HWND IN FRAME {&frame-name}, 410, /* LB_SETITEMDATA */ SelIndex, INTEGER(RECID(customer)), OUTPUT ReturnValue). END. SET-SIZE(lpString)=0. END PROCEDURE.
The first call to SendMessage adds a text to the selection list, so it does the same as select-1:ADD-LAST(TRIM(customer.name)). However it also returns a SelIndex, which is the unique and constant index number for the new list-item. This SelIndex is used in the second call to SendMessage.
The second call to SendMessage adds an integer data value to the list-item.
{windows.i} ON VALUE-CHANGED OF SELECT-1 IN FRAME DEFAULT-FRAME DO: DEFINE VARIABLE SelIndex AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. RUN SendMessageA (select-1:HWND, 392, /* LB_GETCURSEL */ 0, 0, OUTPUT SelIndex). RUN SendMessageA (select-1:HWND, 409, /* LB_GETITEMDATA */ SelIndex, 0, OUTPUT ReturnValue). FIND customer WHERE RECID(customer)=ReturnValue NO-LOCK NO-ERROR. IF AVAIL customer THEN DO: DISPLAY cust-num NAME city country WITH FRAME {&frame-name}. END. END.
Instead of looking at the screen-value I call SendMessage to get the unique SelIndex, and call SendMessage again to get the value of the item-data connected to that SelIndex. Because this item-data value was actually a RECID, I can now identify the customer and display its details.
The SelIndex value for a list-item never changes even when list-items are deleted or inserted, whether or not the selection-list is sorted.
Instead of storing an Integer value in Itemdata, you can also store a POINTER-VALUE pointing to some string or structure. However when you do this you will have to implement some kind of destructor.
Source by Todd Nist, Protech Systems Inc.
Modified by Simon Sweetman
The library COMCTL32.DLL contains common controls like the Status Bar control. This example, provided by Todd Nist, shows how to use the Status Bar control in a Progress window. Simon Sweetman added the progressbar. The example will produce a window that looks like this:
The Status Bar in this example is divided in three parts: the first part contains a progressbar which displays the value of the slider, the second part is shown with SBT_POPOUT style so it appears raised.
Button WriteStatusBar results in sending window-messages to the Status Bar control, in this case message SB_SETTEXT. The control also accepts other messages like SB_SETPARTS (to change the size or count of parts), SB_SETICON (to show an icon in a part) or SB_SETTIPTEXT (to set a tooltip for a part). However SB_SETICON and SB_SETTIPTEXT are only available in comctl32.dll version 4.71 or newer.
/*------------------------------------------------------------------------ File: StatusExample.p Author: Todd Nist, Protech Systems Inc. Created: 5/19/98 Modifications: 28/11/00 Simon Sweetman - Added progress meter control ------------------------------------------------------------------------*/ &SCOP WINDOW-NAME C-Win &SCOP FRAME-NAME DEFAULT-FRAME CREATE WIDGET-POOL. /* *************************** Definitions ************************** */ /* Local Variable Definitions --- */ {windows.i} &SCOPE WM_USER 1024 &SCOPE SB_SETPARTS {&WM_USER} + 4 &SCOPE SB_SETTEXT {&WM_USER} + 1 &SCOPE SB_GETRECT {&WM_USER} + 10 &SCOPE PBM_SETRANGE {&WM_USER} + 1 &SCOPE PBM_SETPOS {&WM_USER} + 2 &SCOPE SBT_NORMAL 0 &SCOPE SBT_POPOUT 512 &SCOPE SBT_NOBORDERS 256 /* variables to hold handle control objects */ DEFINE VARIABLE hStatusBar AS INTEGER NO-UNDO. DEFINE VARIABLE progHWND AS INTEGER NO-UNDO. PROCEDURE CreateStatusWindow EXTERNAL "comctl32.dll": DEFINE INPUT PARAMETER lStyle AS LONG. DEFINE INPUT PARAMETER lpctStr AS CHARACTER. DEFINE INPUT PARAMETER hwndParent AS LONG. DEFINE INPUT PARAMETER wId AS LONG. DEFINE RETURN PARAMETER hStatusArea AS LONG. END PROCEDURE. PROCEDURE InitCommonControls EXTERNAL "comctl32.dll": END PROCEDURE. /* Standard preprocessor definitions */ &SCOPED-DEFINE PROCEDURE-TYPE WINDOW &SCOPED-DEFINE ENABLED-OBJECTS BtnCreateStatus BtnWriteStatus SLIDER-1 /* ************************ Function Prototypes ********************** */ FUNCTION CreateProgressArea RETURNS INTEGER ( INPUT phStatusBar AS INTEGER, INPUT piStatusArea AS INTEGER, INPUT piMaxValue AS INTEGER) FORWARD. FUNCTION CreateStatusBar RETURNS INTEGER ( /* parameter-definitions */ ) FORWARD. FUNCTION SetProgressArea RETURNS LOGICAL ( INPUT phProgressArea AS INTEGER, INPUT piValue AS INTEGER ) FORWARD. FUNCTION WriteStatusArea RETURNS LOGICAL ( INPUT phStatusBar AS INTEGER, INPUT piStatusArea AS INTEGER, INPUT piSBTextMode AS INTEGER, INPUT pcText AS CHARACTER ) FORWARD. /* *********************** Control Definitions ********************** */ /* Define the widget handle for the window */ DEFINE VARIABLE C-Win AS WIDGET-HANDLE NO-UNDO. /* Menu Definitions */ DEFINE SUB-MENU m_File MENU-ITEM m_Exit LABEL "E&xit" . DEFINE MENU MENU-BAR-C-Win MENUBAR SUB-MENU m_File LABEL "File" . /* Definitions of the field level widgets */ DEFINE BUTTON BtnCreateStatus LABEL "Create Status Bar" SIZE 25 BY 1. DEFINE BUTTON BtnWriteStatus LABEL "WriteStatusBar" SIZE 25 BY 1. DEFINE VARIABLE SLIDER-1 AS INTEGER INITIAL 0 VIEW-AS SLIDER MIN-VALUE 0 MAX-VALUE 100 HORIZONTAL SIZE 45 BY 2 NO-UNDO. /* ************************ Frame Definitions *********************** */ DEFINE FRAME {&FRAME-NAME} SLIDER-1 AT ROW 1 COL 7 NO-LABEL BtnCreateStatus AT ROW 6 COL 6 BtnWriteStatus AT ROW 6 COL 50 WITH 1 DOWN NO-BOX KEEP-TAB-ORDER OVERLAY SIDE-LABELS NO-UNDERLINE THREE-D AT COL 1 ROW 1 SIZE 80 BY 8. /* ************************* Create Window ************************** */ IF SESSION:DISPLAY-TYPE = "GUI":U THEN CREATE WINDOW C-Win ASSIGN HIDDEN = YES TITLE = "StatusBar Example" HEIGHT = 8 WIDTH = 80 MAX-HEIGHT = 8 MAX-WIDTH = 80 VIRTUAL-HEIGHT = 8 VIRTUAL-WIDTH = 80 RESIZE = YES SCROLL-BARS = NO STATUS-AREA = NO BGCOLOR = ? FGCOLOR = ? KEEP-FRAME-Z-ORDER = YES THREE-D = YES MESSAGE-AREA = NO SENSITIVE = YES. ELSE {&WINDOW-NAME} = CURRENT-WINDOW. ASSIGN {&WINDOW-NAME}:MENUBAR = MENU MENU-BAR-C-Win:HANDLE. /* *************** Runtime Attributes and UIB Settings ************** */ IF SESSION:DISPLAY-TYPE = "GUI":U AND VALID-HANDLE(C-Win) THEN C-Win:HIDDEN = NO. /* ************************ Control Triggers ************************ */ ON END-ERROR OF C-Win /* */ OR ENDKEY OF {&WINDOW-NAME} ANYWHERE DO: /* This case occurs when the user presses the "Esc" key. In a persistently run window, just ignore this. If we did not, the application would exit. */ IF THIS-PROCEDURE:PERSISTENT THEN RETURN NO-APPLY. END. ON WINDOW-CLOSE OF C-Win /* */ DO: /* This event will close the window and terminate the procedure. */ APPLY "CLOSE":U TO THIS-PROCEDURE. RETURN NO-APPLY. END. ON VALUE-CHANGED OF SLIDER-1 IN FRAME {&FRAME-NAME} /* Create Status Bar */ DO: IF ProgHWND NE 0 THEN SetProgressArea(progHWND, INT(SELF:SCREEN-VALUE) * 10). END. ON CHOOSE OF BtnCreateStatus IN FRAME {&FRAME-NAME} /* Create Status Bar */ DO: hStatusBar = CreateStatusBar(). END. ON CHOOSE OF BtnWriteStatus IN FRAME {&FRAME-NAME} /* WriteStatusBar */ DO: DEFINE VARIABLE i AS i NO-UNDO. DO i = 1 TO 2. WriteStatusArea(hStatusBar, i, IF i = 1 THEN {&SBT_POPOUT} ELSE {&SBT_NORMAL}, SUBSTITUTE('Message &1 here...', STRING(i,'9'))). END. progHWND = CreateProgressArea(hStatusBar, 0, 1000). END. ON CHOOSE OF MENU-ITEM m_Exit /* Exit */ DO: APPLY 'WINDOW-CLOSE':U TO {&WINDOW-NAME}. END. /* *************************** Main Block *************************** */ ASSIGN CURRENT-WINDOW = {&WINDOW-NAME} THIS-PROCEDURE:CURRENT-WINDOW = {&WINDOW-NAME}. ON CLOSE OF THIS-PROCEDURE RUN disable_UI. PAUSE 0 BEFORE-HIDE. MAIN-BLOCK: DO ON ERROR UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK: {win32/struct/rect.i} /* Used by CreateProgressArea function */ RUN enable_UI. IF NOT THIS-PROCEDURE:PERSISTENT THEN WAIT-FOR CLOSE OF THIS-PROCEDURE. END. /* ********************** Internal Procedures *********************** */ PROCEDURE disable_UI : /*----------------------------------------------------------------------------- Purpose: DISABLE the User Interface Parameters: Notes: Here we clean-up the user-interface by deleting dynamic widgets we have created and/or hide frames. This procedure is usually called when we are ready to "clean-up" after running. ------------------------------------------------------------------------------*/ /* Delete the WINDOW we created */ IF SESSION:DISPLAY-TYPE = "GUI":U AND VALID-HANDLE(C-Win) THEN DELETE WIDGET C-Win. IF THIS-PROCEDURE:PERSISTENT THEN DELETE PROCEDURE THIS-PROCEDURE. END PROCEDURE. PROCEDURE enable_UI : /*------------------------------------------------------------------------------ Purpose: ENABLE the User Interface Parameters: none Notes: Here we display/view/enable the widgets in the user-interface. In addition, OPEN all queries associated with each FRAME and BROWSE. These statements here are based on the "Other Settings" section of the widget Property Sheets. ------------------------------------------------------------------------------*/ ENABLE BtnCreateStatus BtnWriteStatus SLIDER-1 WITH FRAME {&FRAME-NAME} IN WINDOW C-Win. {&OPEN-BROWSERS-IN-QUERY-{&FRAME-NAME}} VIEW C-Win. END PROCEDURE. FUNCTION CreateProgressArea RETURNS INTEGER (INPUT phStatusBar AS INTEGER, INPUT piStatusArea AS INTEGER, INPUT piMaxValue AS INTEGER): /*------------------------------------------------------------------------------ Purpose: Create status bar progress meter control Notes: Return value is window handle of control ------------------------------------------------------------------------------*/ DEFINE VARIABLE hwndParent AS INTEGER NO-UNDO. DEFINE VARIABLE hInstance AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. DEFINE VARIABLE ReturnHwnd AS INTEGER NO-UNDO. SET-SIZE(lpRect) = 16. /* allocate space for rect object */ /* Get screen postion of required status bar area */ RUN SendMessageA IN hpApi(INPUT phStatusBar, INPUT {&SB_GETRECT}, INPUT piStatusArea, INPUT GET-POINTER-VALUE(lpRect), OUTPUT ReturnValue). RUN mem2buf_lpRect. SET-SIZE(lpRect) = 0. /* release allocated space */ RUN GetParent IN hpApi({&WINDOW-NAME}:HWND, OUTPUT hwndParent). RUN GetWindowLongA IN hpApi(hwndParent, -6, /* GWL_HINSTANCE */ OUTPUT hInstance). /* Create the progress meter window control and parent it to status window */ RUN CreateWindowExA IN hpApi( 8, /* extended style */ "msctls_progress32":U, /* progress meter class name */ "", /* window name */ 1073741824 /* = WS_CHILD */ + 1 /* = PBS_SMOOTH */ + 268435456, /* = WS_VISIBLE */ /* window styles */ lpRect.LEFT, lpRect.TOP, lpRect.RIGHT - lpRect.LEFT, lpRect.BOTTOM - lpRect.TOP, /* window position & size */ phStatusBar, /* parent window */ 0, /* menu pointer */ hInstance, /* instance */ 0, /* parameters */ OUTPUT ReturnHwnd). /* set maximum value of progress meter control */ RUN SendMessageA IN hpApi(INPUT ReturnHwnd, INPUT {&PBM_SETRANGE}, INPUT 0, INPUT piMaxValue * 65536, OUTPUT ReturnValue). RETURN ReturnHwnd. END FUNCTION. FUNCTION CreateStatusBar RETURNS INTEGER ( /* parameter-definitions */ ) : /*------------------------------------------------------------------------------ Purpose: Notes: ------------------------------------------------------------------------------*/ DEFINE VARIABLE hwndMenu AS INTEGER NO-UNDO. DEFINE VARIABLE hwndParent AS INTEGER NO-UNDO. DEFINE VARIABLE hInstance AS INTEGER NO-UNDO. DEFINE VARIABLE hWindowMenu AS INTEGER NO-UNDO. DEFINE VARIABLE hStatusArea AS INTEGER NO-UNDO. DEFINE VARIABLE lpParam AS MEMPTR NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. /*--------------------------------------------------------------------- Allocate memory and define the array to support the segments of the status area. -----------------------------------------------------------------------*/ ASSIGN SET-SIZE(lpParam) = 256 PUT-LONG(lpParam,1) = 120 PUT-BYTE(lpParam,5) = 240 PUT-LONG(lpParam,9) = -1. /* extend to the right edge of the window */ /* find handle to the Parent handle of the Window */ RUN GetParent IN hpApi({&WINDOW-NAME}:HWND, OUTPUT hwndParent). RUN GetWindowLongA IN hpApi(hwndParent, -6, /* GWL_HINSTANCE */ OUTPUT hInstance). /* Call InitCommonControls to ensure that the comctl32.dll is loaded */ RUN InitCommonControls. /* hwndParent */ /* Create the status window control and parent it to the window */ RUN CreateStatusWindow ( 1073741824 /* = WS_CHILD */ + 268435456 /* = WS_VISIBLE */ /* window styles */ + 8388608, /* = WS_BORDER */ '', /* default text */ hwndParent, /* parent window */ 101, /* ID_STATUS */ OUTPUT hStatusArea). IF hStatusArea = 0 THEN MESSAGE 'Unable to create the status bar...' VIEW-AS ALERT-BOX. ELSE /* create the multiple segments based on the data in lpParam */ RUN SendMessageA IN hpApi( INPUT hStatusArea, INPUT {&SB_SETPARTS}, INPUT 3, /* number of parts */ INPUT GET-POINTER-VALUE(lpParam), OUTPUT ReturnValue). /* deallocate the memory */ ASSIGN SET-SIZE(lpParam) = 0. RETURN hStatusArea. /* Function return value. */ END FUNCTION. FUNCTION SetProgressArea RETURNS LOGICAL ( INPUT phProgressArea AS INTEGER, INPUT piValue AS INTEGER ) : /*------------------------------------------------------------------------------ Purpose: Update progress indicator value and redisplay Notes: ------------------------------------------------------------------------------*/ DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. RUN SendMessageA IN hpApi(INPUT phProgressArea, INPUT {&PBM_SETPOS}, INPUT piValue, INPUT 0, OUTPUT ReturnValue). RETURN TRUE. END FUNCTION. FUNCTION WriteStatusArea RETURNS LOGICAL ( INPUT phStatusBar AS INTEGER, INPUT piStatusArea AS INTEGER, INPUT piSBTextMode AS INTEGER, INPUT pcText AS CHARACTER ) : /*------------------------------------------------------------------------------ Purpose: Write the text in the appropriat format to the appropriate area Notes: ------------------------------------------------------------------------------*/ DEFINE VARIABLE lpParam AS MEMPTR NO-UNDO. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. ASSIGN SET-SIZE(lpParam) = 256 PUT-STRING(lpParam,1) = pcText. RUN SendMessageA IN hpApi(INPUT phStatusBar, INPUT {&SB_SETTEXT}, INPUT piStatusArea + piSBTextMode, INPUT GET-POINTER-VALUE(lpParam), OUTPUT ReturnValue). /* deallocate the memory */ ASSIGN SET-SIZE(lpParam) = 0. RETURN TRUE. END FUNCTION.
message SB_SETTEXT has the following parameters:
* wParam = iPart + uType
* lParam = text. The text for each part is limited to 127 characters.
* iPart is the index of the part to set. If iPart=255 you will get a 'simple' status bar with only one part.
uType can be:
* {&SBT_NORMAL} : border, appears lower
* {&SBT_NOBORDERS} : no border, no 3D effect
* {&SBT_POPOUT} : border, appears higher
The Status Bar has a sizing grip because it is parented to the resizable window. It will not have a sizing grip when you parent it to the frame :
/* Create the status window control and parent it to the frame */ RUN CreateStatusWindow ( 1073741824 /* = WS_CHILD */ + 268435456 /* = WS_VISIBLE */ /* window styles */ + 8388608, /* = WS_BORDER */ '', /* default text */ FRAME {&frame-name}:HWND, /* parent */ 101, /* ID_STATUS */ OUTPUT hStatusArea).
This also solves the repaint problem: if the status bar is parented to the window, it will appear to be invisible after the window is repainted.
There's another way to solve the repaint problem, and to keep the StatusBar parented to the window (and therefore keep the "sizing grip").
Simply run:
RUN SendMessageA IN hpApi(INPUT phStatusBar, INPUT 5, /* WM_SIZE */ INPUT 0, INPUT 0, OUTPUT ReturnValue).
For instance, at the end of a WINDOW-RESIZED event (or after a "ShowScrollBar")
The functions GetSysColor and SetSysColors can be used to access the system colors.
This is for example useful for displaying text in disabled native fill-in widgets.
Text in a disabled native fill-in is rendered in the system-color COLOR_GRAYTEXT, which is gray. The background color of a disabled fill-in is also gray (but not COLOR_GRAYTEXT) so it is difficult to read the text.
To make the text more readable you may want to change the RGB-value of the COLOR_GRAYTEXT system color.
There are several ways to change COLOR_GRAYTEXT:
You can set or modify the registry key
"HKEY_CURRENT_USER\Control Panel\Colors\GrayText"
but that will only take effect after you reboot the system.
You can also consider using the SetSysColors function to modify the RGB-value of COLOR_GRAYTEXT. The advantage is that it will take effect immediately, no need to reboot the system.
The disadvantage is that it won't be written to registry so the original value is reset after reboot. Well, actually I think this is another advantage.
What I would try to do is:
on startup of the Progress session: read the current value of COLOR_GRAYTEXT
if COLOR_GRAYTEXT is not acceptable set it to something you prefer.
on close of the Progress session: restore the original value to respect other applications.
After you call SetSysColors, Windows sends a notification message to all open windows so they can repaint themselves. This takes a little time.
Read COLOR_GRAYTEXT
{windows.i} DEFINE VARIABLE rgbGrayText AS INTEGER NO-UNDO. RUN GetSysColor IN hpApi (17, /* = COLOR_GRAYTEXT */ OUTPUT rgbGrayText).
set COLOR_GRAYTEXT
{windows.i} DEFINE VARIABLE lpElements AS MEMPTR. DEFINE VARIABLE lpRgb AS MEMPTR. DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO. SET-SIZE(lpElements) = 4. /* = sizeof(long) */ SET-SIZE(lpRgb) = 4. PUT-LONG(lpElements,1) = 17. /* = COLOR_GRAYTEXT */ PUT-LONG(lpRgb,1) = RGB-VALUE(192,192,192). RUN SetSysColors IN hpApi (1, /* = number of elements */ GET-POINTER-VALUE(lpElements), GET-POINTER-VALUE(lpRgb), OUTPUT ReturnValue). SET-SIZE(lpElements) = 0. SET-SIZE(lpRgb) = 0.
Credits to Julian Lyndon Smith who discovered the value 192,192,192. Although it is the RGB-value for a shade of gray, it will render the text in disabled native fill-in widgets black but still does little harm to other screen-elements that are coloured in COLOR_GRAYTEXT.
(May 1999).
It now appears that setting GrayText to RGB(192,192,192) makes disabled SELECTION-LIST widgets really hard to read. Don't know why it was not noticed before... perhaps it depends on Windows 98, video card or other display settings? Anyway, GrayText=RGB(192,192,192) does not seem to be a good idea anymore.
KeithGernert 19 Jan 2004: We've found that an RGB setting of 80,80,80 looks pretty good and is less destructive to other Windows controls.
A standard toggle-box widget has its label on the right. This example applies the BS_LEFTTEXT style to bring the label to the left.
As you can see in the picture, the checkboxes are not vertically aligned anymore. Tex Texin wrote a
procedure to fix this: see http:www.xencraft.com/resources/rightside-checkbox.html
RUN ToggleLeftText (toggle-1:HWND). RUN ToggleLeftText (toggle-2:HWND).
PROCEDURE ToggleLeftText : /* ------------------------------------------------------------- purpose: place the label on the left side. do not run this procedure more than once for each toggle-box ------------------------------------------------------------- */ DEFINE INPUT PARAMETER HWND AS INTEGER. DEFINE VARIABLE styles AS INTEGER NO-UNDO. DEFINE VARIABLE returnvalue AS INTEGER NO-UNDO. RUN GetWindowLongA(HWND, {&GWL_STYLE}, OUTPUT styles). styles = styles + {&BS_LEFTTEXT}. RUN SetWindowLongA(HWND, {&GWL_STYLE}, styles, OUTPUT styles). /* force a repaint */ RUN InvalidateRect(HWND,0,1,OUTPUT returnvalue). END PROCEDURE.
Definitions used in this example:
&GLOBAL-DEFINE GWL_STYLE -16 &GLOBAL-DEFINE BS_LEFTTEXT 32 PROCEDURE GetWindowLongA EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER phwnd AS LONG. DEFINE INPUT PARAMETER cindex AS LONG. DEFINE RETURN PARAMETER currentlong AS LONG. END PROCEDURE. PROCEDURE SetWindowLongA EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER phwnd AS LONG. DEFINE INPUT PARAMETER cindex AS LONG. DEFINE INPUT PARAMETER newlong AS LONG. DEFINE RETURN PARAMETER oldlong AS LONG. END PROCEDURE. PROCEDURE InvalidateRect EXTERNAL "user32.dll" : DEFINE INPUT PARAMETER HWND AS LONG. DEFINE INPUT PARAMETER lpRect AS LONG. DEFINE INPUT PARAMETER bErase AS LONG. DEFINE RETURN PARAMETER ReturnValue AS LONG. END PROCEDURE.