I'm trying to develop a remote file transfer. The server application downloads files to a temporary folder. At the very beginning of the download, SetClipboardData(CF_HDROP, NULL)
is called. And then pressing Ctrl-V is simulated, in order to transfer file paths in the WM_RENDERFORMAT
handler after downloading the files. The problem is that the WM_RENDERFORMAT
message arrives even without Ctrl-V simulation. I noticed that if there are no open folders at the start of the program, it does not come, if there is at least one opened folder, the message will definitely come. It's likely that I'm wrong somewhere, but I can't figure out where.
At first I created my own MyDataObject
class based on IDataObject
(Raymond Chan has written some excellent articles on this topic - https://github.com/mity/old-new-win32api#drag-and-drop).
In fact, all the difference between my class and CTinyDataObject
from Raymond's articles in the constructor and the GetData
method.
Constructor:
MyDataObject::MyDataObject(std::vector<FileInfo>& fileList) :
refCnt_(1),
fileList_(fileList) {
FORMATETC format;
SetFORMATETC(&format, RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR));
formats_.push_back(format);
for (int i = 0; i < fileList.size(); i++) {
SetFORMATETC(&format, RegisterClipboardFormat(CFSTR_FILECONTENTS), TYMED_ISTREAM, i);
formats_.push_back(format);
}
}
fileList_
- a class variable that stores a vector of such structures
struct FileInfo {
int fileSize;
std::wstring path;
std::wstring fileName;
bool canOpenFile = false;
IStream* stream_ = nullptr;
};
GetData
:
HRESULT MyDataObject::GetData(FORMATETC* pfe, STGMEDIUM* pmed) {
ZeroMemory(pmed, sizeof(*pmed));
auto format = GetDataIndex(pfe);
if (format == DATA_FILEGROUPDESCRIPTOR) {
int fgd_size = sizeof(FILEGROUPDESCRIPTOR) + (fileList_.size() - 1) * sizeof(FILEDESCRIPTOR);
LPFILEGROUPDESCRIPTOR fgd = (LPFILEGROUPDESCRIPTOR)new BYTE[fgd_size];
ZeroMemory(fgd, sizeof(fgd));
fgd->cItems = fileList_.size();
for (int i = 0; i < fileList_.size(); i++) {
fgd->fgd[i].dwFlags = FD_FILESIZE;
fgd->fgd[i].nFileSizeHigh = 0;
fgd->fgd[i].nFileSizeLow = fileList_[i].fileSize;
StringCchCopy(fgd->fgd[i].cFileName, ARRAYSIZE(fgd->fgd[i].cFileName), fileList_[i].fileName.c_str());
}
pmed->tymed = TYMED_HGLOBAL;
return CreateHGlobalFromBlob(fgd, fgd_size, GMEM_MOVEABLE, &pmed->hGlobal);
}
else if (format > DATA_FILEGROUPDESCRIPTOR && format <= DATA_FILEGROUPDESCRIPTOR + fileList_.size()) {
pmed->tymed = TYMED_ISTREAM;
fileList_[pfe->lindex].stream_ = new MyIStream();
if (fileList_[pfe->lindex].canOpenFile) {
MyIStream* stream = (MyIStream*)fileList_[pfe->lindex].stream_;
stream->OpenFile(fileList_[pfe->lindex].path.c_str());
}
pmed->pstm = fileList_[pfe->lindex].stream_;
return S_OK;
}
return DV_E_FORMATETC;
}
In order to express the contents of a virtual file, I defined my IStream class. It also does not differ much from the standard:
class MyIStream : public IStream {
public:
HRESULT OpenFile(LPCWSTR pName);
....
private:
std::mutex mutex_;
HANDLE _hFile = nullptr;
};
HRESULT MyIStream::OpenFile(LPCWSTR pName) {
std::lock_guard<std::mutex> guard(mutex_);
_hFile = ::CreateFile(pName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (_hFile == INVALID_HANDLE_VALUE)
return HRESULT_FROM_WIN32(GetLastError());
return S_OK;
}
HRESULT STDMETHODCALLTYPE MyIStream::Read(void* pv, ULONG cb, ULONG* pcbRead) {
while (true) {
{
std::lock_guard<std::mutex> guard(mutex_);
if (_hFile) {
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
BOOL rc = ReadFile(_hFile, pv, cb, pcbRead, NULL);
return S_OK;
}
How I use this in my main app. Initially, I get a complete list of files to download from the client and create a separate thread:
void MyAppClass::fileCopyThread() {
if (SUCCEEDED(OleInitialize(NULL))) {
fileCopyThreadId_ = GetCurrentThreadId();
IDataObject* pdto = new MyDataObject(filesDesc_);
if (pdto) {
OleSetClipboard(pdto);
pdto->Release();
}
// simulate Ctrl-V
...
// process messages
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
OleSetClipboard(NULL);
OleUninitialize();
}
}
When the file is fully loaded, I call the FileInfo.OpenFile
method (or set the FileInfo.canOpenFile
flag to true if MyDataObject::GetData has not been called yet - this happens, for example, if the file is small and was downloaded very fast).
After all files from the remote are downloaded we need to call this:
PostThreadMessage(fileCopyThreadId_, WM_QUIT, 0, 0);