Question

I need to pass a string longer than 32/64KiB to the clipboard from my program and since the built in CLIPBOARD function in OpenEdge has that as a limit I have to resort to using DLL calls.

The strange thing is that everything works fine... once.. but if I try to do it twice in a program then the program crashes. I'm using OpenEdge 11.3.1 and also tried it in 10.2B which seems to work better but gives a different crash message.

I have tried moving things around, not emptying the clipboard (according to MS I shouldn't empty, but without emptying it doesn't work), changing the OpenClipboard function to use CURRENT-WINDOW:HWND instead of 0 and nothing changes.

As I said everything works fine once and the clipboard is filled with my text.. but if I try to OpenClipboard again the same program then it crashes without fail.

After reading the API manual with a fine-toothed comb I think I have found the problem:

After SetClipboardData is called, the system owns the object identified by the hMem parameter. The application can read the data, but must not free the handle or leave it locked until the CloseClipboard function is called. (The application can access the data after calling CloseClipboard). If the hMem parameter identifies a memory object, the object must have been allocated using the GlobalAlloc function with the GMEM_MOVEABLE flag.

I don't know if there is any way in OpenEdge to allocate global memory so I'm stumped. If I just don't release the memory pointer then I can open the clipboard again but I can't reuse the variable since Progress doesn't understand the variable isn't its own any more. The second SET-SIZE has no effect, even though mRet is a local variable in the function it doesn't seem to get reset with every call to the function.

/* Clipboard Crash Test */
ROUTINE-LEVEL ON ERROR UNDO, THROW.
SESSION:ERROR-STACK-TRACE = TRUE.

PROCEDURE OpenClipboard EXTERNAL 'user32.dll':
    DEFINE INPUT  PARAMETER hWndNewOwner    AS LONG NO-UNDO.
    DEFINE RETURN PARAMETER lRet            AS LONG NO-UNDO.
END PROCEDURE.

PROCEDURE CloseClipboard EXTERNAL 'user32.dll':
    DEFINE RETURN PARAMETER lRet            AS LONG NO-UNDO.
END PROCEDURE.

PROCEDURE EmptyClipboard EXTERNAL 'user32.dll':
    DEFINE RETURN PARAMETER lRet            AS LONG NO-UNDO.
END PROCEDURE.

PROCEDURE SetClipboardData EXTERNAL 'user32.dll':
    DEFINE INPUT  PARAMETER uFormat     AS LONG      NO-UNDO.
    DEFINE INPUT  PARAMETER hMem        AS LONG      NO-UNDO.
    DEFINE RETURN PARAMETER uRet        AS LONG      NO-UNDO.
END PROCEDURE.

FUNCTION SetClipboardText RETURNS LOGICAL (cText AS LONGCHAR):
    DEFINE VARIABLE iRet AS INT64   NO-UNDO.
    DEFINE VARIABLE mRet AS MEMPTR  NO-UNDO.
    DEFINE VARIABLE lRet AS LOGICAL NO-UNDO.

    RUN OpenClipboard(0, OUTPUT iRet).
    IF iRet <> 0 THEN 
    DO:
        RUN EmptyClipboard(OUTPUT iRet) NO-ERROR. 
        SET-SIZE(mRet) = LENGTH(cText,'RAW') + 1.
        PUT-STRING(mRet,1) = cText.
        RUN SetClipboardData(1, GET-POINTER-VALUE(mRet), OUTPUT iRet).
        IF iRet <> 0 THEN lRet = TRUE.
/*      SET-SIZE(mRet) = 0.*/
        RUN CloseClipboard(OUTPUT iRet) NO-ERROR.
    END.
    RETURN lRet.
END FUNCTION.

DEFINE VARIABLE cText AS LONGCHAR NO-UNDO.

ASSIGN cText = 'Text'.

SetClipboardText(cText).

MESSAGE "Clipboard set once." VIEW-AS ALERT-BOX.

ASSIGN cText = 'Newt'.

SetClipboardText(cText).

MESSAGE "Clipboard set twice." VIEW-AS ALERT-BOX.
Was it helpful?

Solution 2

After contacting Progress Support I got a version that works, mostly by bypassing the built in functions in Progress altogether.

FUNCTION SetClipboardText RETURNS LOGICAL (cText AS LONGCHAR):
    DEFINE VARIABLE iRet AS INT64   NO-UNDO INIT 0.
    DEFINE VARIABLE mRet AS MEMPTR  NO-UNDO.
    DEFINE VARIABLE lRet AS LOGICAL NO-UNDO INIT FALSE.
    DEFINE VARIABLE iHnd AS INT64   NO-UNDO INIT 0.
    DEFINE VARIABLE iPtr AS INT64   NO-UNDO INIT 0.

    /* Open the clipboard for processing */    
    RUN OpenClipboard(0, OUTPUT iRet).
    IF iRet <> 0 THEN 
    DO:
        /* Tell the clipboard to clear itself */
        RUN EmptyClipboard(OUTPUT iRet) NO-ERROR. 
        /* Globally allocate memory for the clipboard data */
        RUN GlobalAlloc(2, LENGTH(cText,'RAW') + 1, OUTPUT iHnd).
        RUN GlobalLock(iHnd, OUTPUT iPtr).
        /* Assign the global memory to the memory pointer */
        SET-POINTER-VALUE(mRet) = iPtr.
        /* Copy the supplied value to the global memory region */
        PUT-STRING(mRet,1) = cText.
        /* Unlock the memory so that clipboard can read it */
        RUN GlobalUnlock(iHnd, OUTPUT iPtr).
        /* Tell the clipboard to copy the data */        
        RUN SetClipboardData(1, iHnd, OUTPUT iRet).
        IF iRet <> 0 THEN lRet = TRUE.
        /* Close the clipboard */
        RUN CloseClipboard(OUTPUT iRet) NO-ERROR.
        /* Free the memory once the clipboard is closed */
        IF iHnd <> 0 THEN
            RUN GlobalFree(iHnd, OUTPUT iRet).
    END.
    RETURN lRet.
END FUNCTION.

OTHER TIPS

Yes, there is some limitation in the way Progress handles the clipboard.

There's a note in the online help:

Note: In Windows, the clipboard can store a maximum of 64K of data.

So yes, there's a limitation that will force you to do this another way.

This has most likely something to do with the raw variable being in use when you empty it.

If I remove

SET-SIZE(mRet) = 0.

I'm able to open the clipboard again.

Based on the entry from the Knowledge base (see below) I'm guessing that the dll has already deallocated the memptr and thus there's no need for you to do it (or rather - deallocating again leads to a crash). So simply removing the deallocation should really fix it.

Knowledgebase Entry.

Reading in MSDN you can see that the "system" owns the pointer once SetClipboardData is called. Thus one viable solution must be to create a new pointer each time. Store the pointers in an array, and free them up when quitting.

From MSDN:

If SetClipboardData succeeds, the system owns the object identified by the hMem parameter. The application may not write to or free the data once ownership has been transferred to the system, but it can lock and read from the data until the CloseClipboard function is called. (The memory must be unlocked before the Clipboard is closed.) If the hMem parameter identifies a memory object, the object must have been allocated using the function with the GMEM_MOVEABLE flag.

Full text here

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top