3

I'm a complete beginner at C++, but I've managed to modify other people's code and finally put together a program that writes the Windows clipboard to a text file, with the text encoded in the codepage specified as a command line parameter for the program (for example, MyProg.exe 437) will write the text in codepage 437. If no codepage is specified, the program writes a text file in the standard Windows codepage 1252.

The trouble is that the program crashes if the clipboard contains (for example) a shortcut or file, not text. Can someone tell me how I can exit gracefully if there is no CF_TEXT data in the clipboard? (Or have I completely misunderstood the problem?)

I've searched for an answer with no success. Here is my VC2010 code (which I don't entirely understand, but it seems to work, when the clipboard contains CF_TEXT; it doesn't work with CF_UNICODETEXT, by the way - the output is simply a few bytes.):

#include <stdafx.h>
#include <windows.h>
#include <iostream>
#include <fstream>
#include <codecvt> // for wstring_convert
#include <locale>  // for codecvt_byname

using namespace std;

void BailOut(char *msg)
{
fprintf(stderr, "Exiting: %s\n", msg);
exit(1);
}

int main(int argc, char* argv[])
{
std::string codepage = ".1252";
if (argc > 1) {
    std::string cpnum = argv[1];
    codepage = "."+cpnum;   
}

HANDLE clip;
string clip_text = "";
// exit if clipboard not available
if (!OpenClipboard(NULL))
        BailOut("Can't open clipboard"); 

clip = GetClipboardData(CF_TEXT);
clip_text = (char*)clip;
CloseClipboard();

// create conversion routines
typedef std::codecvt_byname<wchar_t,char,std::mbstate_t> codecvt;
std::wstring_convert<codecvt> cp1252(new codecvt(".1252"));
std::wstring_convert<codecvt> outpage(new codecvt(codepage));

std::string OutFile = "#clip.txt"; // output file name
ofstream OutStream;  // open an output stream
OutStream.open(OutFile, ios::out | ios::trunc);

// make sure file is successfully opened
if(!OutStream)
{
    cout << "Error opening file " << OutFile << " for writing.\n";
    return 1;
}
// convert to DOS/Win codepage number in "outpage"
OutStream << outpage.to_bytes(cp1252.from_bytes(clip_text)).c_str();
OutStream << endl;
OutStream.close(); // close output stream
return 0;
} 

I'll be grateful for any suggestions on how to fix this last problem.

Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
emendelson
  • 416
  • 5
  • 18

2 Answers2

5

Before you get data from the clipboard using the GetClipboardData function, you have to check that the clipboard contains data in the format that you expect. This can be done using the IsClipboardFormatAvailable function as follows:

if(IsClipboardFormatAvailable(CF_TEXT))
{
    // The clipboard contains null-terminated ANSI string.
}
else if (IsClipboardFormatAvailable(CF_UNICODETEXT))
{
    // The clipboard contains null-terminated Unicode string.
}

Then you have to open the clipboard and obtain a handle to the clipboard buffer as follows (assuming the clipboard contains ANSI string):

if(!OpenClipboard(NULL)) return;
HGLOBAL hglb = GetClipboardData(CF_TEXT);

Then you have to obtain exclusive access to the data using the GlobalLock function . Only on success you can safely access the data

if (hglb != NULL)
{
    LPTSTR lptstr = GlobalLock(hglb); 
    if (lptstr != NULL) 
    {
        // Read the contents of lptstr which just a pointer to the string.

        // Don't forget to release the lock after you are done.
        GlobalUnlock(hglb);
    }
}
CloseClipboard(); 

The return type of GlobalLock is a void pointer. So depending on the data format which you have already determined using IsClipboardFormatAvailable, you have to cast it to the corresponding type. Windows data types are documented here.

Hadi Brais
  • 22,259
  • 3
  • 54
  • 95
  • IsClipboardFormatAvailable is useful for setting the UI (for example, determining whether a drop-down menu should offer a Paste option), but using it to ensure that GetClipboardData will succeed is sketchy. Unless you've already opened the clipboard, the data can change between the time you check and the time you actually call GetClipboardData. The real fix, which this answer shows without comment, is to check the return value from GetClipboardData before trying to dereference it. – Adrian McCarthy Jun 29 '15 at 18:01
  • @AdrianMcCarthy Yes. That's why `OpenClipboard` has to be called before `GetClipboardData` to prevent other programs from modifying the clipboard until `CloseClipboard` is called. In this case, `GetClipboardData` will only fail if the format of the data in the clipboard is different from or cannot be implicitly converted to the specified format. – Hadi Brais Jun 29 '15 at 18:36
  • I would like to add also that if the handle returned from `GetClipboardData` is itself a pointer to the first byte of the data (such as when the data format is CF_BITMAP) then `GlobalLock` should not be called. That pointer however should be checked against `NULL`. – Hadi Brais Jun 29 '15 at 18:42
  • If you call (1) IsClipboardFormatAvailable, (2) OpenClipboard, and (3) GetClipboardData (as your answer suggests), then there's a race condition between 1 and 2, which is the point I was trying to make. – Adrian McCarthy Jun 29 '15 at 21:07
  • @AdrianMcCarthy While the data format might change between 1 and 2, that would not create a race hazard because `GetClipboardData` would return NULL then. The data itself might change but this is not a problem since we don't know the data that was there in the first place until `lptstr` is accessed which happens only after `OpenClipboard` succeeds. Also 2 should not called before 1 since this increases the unavailability of the clipboard unnecessarily. – Hadi Brais Jun 30 '15 at 05:37
  • 1
    If you read my original comment carefully, you'll find that's exactly what I said. – Adrian McCarthy Jun 30 '15 at 14:53
0

Thank you for that answer, which is far more useful than the one I came up with before I saw your post. Here is my awkward and unsafe solution (which replaces the corresponding code in my question). I'm going to use your far better solution instead!

HANDLE clip;
std::string clip_text = "";
// exit if clipboard not available
if (!OpenClipboard(NULL))
        BailOut("Can't open clipboard"); 

UINT textOK = 0;
UINT currentFormat = 0;
    while(currentFormat = EnumClipboardFormats(currentFormat)) 
{
    if(currentFormat == 1) 
        textOK = 1;
}
// only get text if text exists in clipboard
if (textOK == 1)
{
    clip = GetClipboardData(CF_TEXT);
    clip_text = (char*)clip;
}
CloseClipboard();

For various reasons (the program that launches this expects to find a #CLIP.TXT file), I've made my code produce an empty file rather than exiting. Thank you again!

emendelson
  • 416
  • 5
  • 18
  • Now, I'm afraid I'm too much of a beginner to figure out how to solve this: "The return type of GlobalLock is a void pointer. So depending on the data format which you have already determined using isClipboardFormatAvailable, you have to cast it to the corresponding type." If I'm using CF_TEXT, where do I add a type into the code, and which type? I'm sorry to come back a second time for this... – emendelson Jun 29 '15 at 16:26
  • Check out the last code fragment in the answer. The type that corresponds to `CF_TEXT` is `LPTSTR`. Also check out the link mentioned at the end of the answer for a list of all built-in Windows types. – Hadi Brais Jun 29 '15 at 16:32
  • Again, forgive me for being thickheaded; you've already helped enormously. I'm using your exact code fragment, but the VC2010 compiler gives me this error message: error C2440: 'initializing' : cannot convert from 'LPVOID' to 'LPTSTR' 1> Conversion from 'void*' to pointer to non-'void' requires an explicit cast This refers to the line: LPTSTR lptstr = GlobalLock(hglb); So I'm a bit lost here. Apologies again for wasting your time with my ignorance! – emendelson Jun 29 '15 at 16:38
  • Note that `LPTSTR` corresponds to either `LPSTR` for ANSI strings or `LPWSTR` for Unicode strings. Maybe you should use these types directly. That is, use `LPSTR` in this case. – Hadi Brais Jun 29 '15 at 16:39
  • 1
    Try this `LPSTR lptstr = (LPSTR)GlobalLock(hglb);` in C++. – Hadi Brais Jun 29 '15 at 16:39
  • That was the answer! It would have taken me six months to figure it out. Thank you again for your patience and expertise! – emendelson Jun 29 '15 at 16:42