I'm trying to implement explorer context menu for my own Windows application.
I was able to show an appropriate context menu for a single file by following this series of articles:
https://devblogs.microsoft.com/oldnewthing/20040920-00/?p=37823
However, I don't think the same approach can be used when multiple files are selected because the implementation is passing a pidl of a single file.
I tested how it worked in Windows explorer. Selecting multiple files and right-clicking worked correctly; It copied selected files or passed selected files to sendto shortcuts. I'm quite not sure how it is done.
Is it possible to do the same thing with iContextMenu interface or is explorer using some private APIs to achieve this behavior?
Here is the code snippet I'm currently using. This assumes that path stores the containing folder and file stores the target file's base name.
IShellFolder *desktop, *fld;
HRESULT ret = SHGetDesktopFolder(&desktop);
if (ret != S_OK)
{
return 0;
}
DWORD chEaten;
DWORD dwAttributes;
LPITEMIDLIST pidl;
ret = desktop->ParseDisplayName(NULL, NULL, const_cast<wchar_t *>(path.c_str()), &chEaten, &pidl, &dwAttributes);
if (ret != S_OK)
{
desktop->Release();
return 0;
}
if (desktop->BindToObject(pidl, NULL, IID_IShellFolder, (void **)&fld) != S_OK)
{
desktop->Release();
return 0;
}
ret = fld->ParseDisplayName(NULL, NULL, const_cast<wchar_t *>(file.c_str()), &chEaten, &pidl, &dwAttributes);
if (ret != S_OK)
{
desktop->Release();
fld->Release();
return 0;
}
const _ITEMIDLIST *child = ILFindLastID(pidl);
ret = fld->GetUIObjectOf(NULL, 1, &child, IID_IContextMenu, NULL, (void **)&contextMenu);
if (ret != S_OK)
{
CoTaskMemFree(pidl);
desktop->Release();
fld->Release();
return 0;
}
contextMenu->QueryContextMenu(contextMenuHandle, 0, 101, 0x7fff, CMF_NORMAL);
contextMenu->QueryInterface(IID_IContextMenu2, (void **)&g_pcm2);
contextMenu->QueryInterface(IID_IContextMenu3, (void **)&g_pcm3);
Related
Consider the following code:
bool OpenStream(const std::wstring& fileName)
{
PIDLIST_ABSOLUTE pidl = nullptr;
if (FAILED(::SHILCreateFromPath(fileName.c_str(), &pidl, nullptr)))
return false;
wchar_t buffer[MAX_PATH + 1];
if (::SHGetPathFromIDListW(pidl, buffer))
{
::OutputDebugString(L"File IDL path: ");
::OutputDebugString(buffer);
::OutputDebugString(L"\r\n");
}
IShellFolder* pShellfolder = nullptr;
LPCITEMIDLIST pidlRelative = nullptr;
HRESULT hr = ::SHBindToParent(pidl, IID_IShellFolder, (void**)&pShellfolder, &pidlRelative);
if (FAILED(hr))
{
::CoTaskMemFree(pidl);
return false;
}
if (::SHGetPathFromIDListW(pidl, buffer))
{
::OutputDebugString(L"Relative IDL path: ");
::OutputDebugString(buffer);
::OutputDebugString(L"\r\n");
}
IStream* pStream = nullptr;
//if (FAILED(pShellfolder->BindToObject(pidlRelative, NULL, IID_IStream, (void**)&pStream)))
if (FAILED(pShellfolder->BindToStorage(pidlRelative, NULL, IID_IStream, (void**)&pStream)))
{
pShellfolder->Release();
::CoTaskMemFree(pidl);
return false;
}
ULARGE_INTEGER size;
::IStream_Size(pStream, &size);
LARGE_INTEGER pos = {0};
pStream->Seek(pos, STREAM_SEEK_SET, nullptr);
unsigned char* pBuffer = new unsigned char[size.QuadPart];
ULONG actualRead;
hr = pStream->Read(pBuffer, size.QuadPart, &actualRead);
std::FILE* pFile;
fopen_s(&pFile, "__Copy.bin", "wb");
if (!pFile)
{
delete[] pBuffer;
pShellfolder->Release();
::CoTaskMemFree(pidl);
return false;
}
const std::size_t writeCount = std::fwrite(pBuffer, sizeof(unsigned char), size.QuadPart, pFile);
std::fclose(pFile);
delete[] pBuffer;
pStream->Seek(pos, STREAM_SEEK_SET, nullptr);
hr = pStream->Write("Test-test-test-test", 19, nullptr);
pShellfolder->Release();
::CoTaskMemFree(pidl);
return true;
}
This code opens the file passed in fileName in a stream and write its content in a new file, using the std to achieve that. Until here all works fine.
However, as a last operation, I want to modify the content of the opened file. However I cannot do that with the above code, indeed it compiles and runs, but it does nothing, and I receive an ACCESS_DENIED error as a result.
How should I modify the above code to allow the opened stream to read AND WRITE in my file?
Also, as a side question: Is the above code safe and well written (i.e will it generates memory leaks, or is something unsafe in it)? A detailed review would be welcome.
Your code doesn't work because you're implicitly opening the stream as read-only. To open it for write operations, you must pass a binding context, something like this:
IBindCtx* ctx;
CreateBindCtx(0, &ctx);
BIND_OPTS options = {};
options.cbStruct = sizeof(BIND_OPTS);
options.grfMode = STGM_WRITE;
ctx->SetBindOptions(&options);
...
pShellfolder->BindToStorage(pidlRelative, ctx, IID_IStream, (void**)&pStream);
...
Otherwise, when you work with COM objects, you should smart pointers to avoid COM reference leaks. For example, below, I use ATL that comes with Visual Studio, but there's also WRL and C++/WinRT (which can do some basic COM stuff too)
Also, for Shell objects it's much easier to work with IShellItem and friends and all ("Item") APIs that come with it, like this (error checks omitted here but you should test every single HRESULT for error) and avoid the old IShellFolder:
CComPtr<IShellItem> item;
SHCreateItemFromParsingName(fileName.c_str(), nullptr, IID_PPV_ARGS(&item));
CComPtr<IBindCtx> ctx;
CreateBindCtx(0, &ctx);
BIND_OPTS options = {};
options.cbStruct = sizeof(BIND_OPTS);
options.grfMode = STGM_WRITE;
ctx->SetBindOptions(&options);
CComPtr<IStream> stream;
item->BindToHandler(ctx, BHID_Stream, IID_PPV_ARGS(&stream));
stream->Write("Test-test-test-test", 19, nullptr);
I have a printer connected that doesn't have a driver and doesn't show up under printers, but it shows up under "Start->Settings->Bluetooth & other devices" with name "SRP300".
I can send data to the printer via the following routine (found here : https://www.levelextreme.com/ViewPageGenericLogin.aspx?LoadContainer=1&NoThread=1157607 ) where it gets the Device Instance ID, and Guid - but I'm simply not able to figure out where I am to get the name from "SP300".
What would I need to call as soon as I've found the GUID of it? The best would be if I could search for the name to start with and if SP300 is found then get the instance id/guid, but I've tried different approaches enumerating to get that name that is shown but nothing seem to produce it.
If I inspect the registry I can see that it's grouped under USB and then under a folder called USBPRINT and then a folder 00000001 and in there there is the name, but wonder how I'm able to retrieve this with Win api calls?
int test2()
{
int MemberIndex = 0;
LONG Result = 0;
DWORD Length = 0;
HANDLE hDevInfo;
ULONG Required;
HANDLE m_hComm=NULL;
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = NULL;
SP_DEVICE_INTERFACE_DATA devInfoData;
hDevInfo = SetupDiGetClassDevs((LPGUID)&(USB_PRINT), NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
if (hDevInfo == INVALID_HANDLE_VALUE)
{
printf("No hardware device");
return 0;
}
devInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
//Step through the available devices looking for the one we want.
do
{
//[1]
Result = SetupDiEnumDeviceInterfaces(hDevInfo, 0, (LPGUID)&(USB_PRINT), MemberIndex, &devInfoData);
if (Result != 0)
{
SetupDiGetDeviceInterfaceDetail(hDevInfo, &devInfoData, NULL, 0, &Length, NULL);
//Allocate memory for the hDevInfo structure, using the returned Length.
detailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)new BYTE[Length * 4];
//Set cbSize in the detailData structure.
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
//Call the function again, this time passing it the returned buffer size.
if (SetupDiGetDeviceInterfaceDetail(hDevInfo, &devInfoData, detailData, Length, &Required, NULL) == TRUE)
{
m_hComm = CreateFile(detailData->DevicePath,
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING, 0, NULL);
if (m_hComm != INVALID_HANDLE_VALUE)
{
//Result = 0;
printf("USB port Available");
}
CloseHandle(m_hComm);
}
delete(detailData);
}
MemberIndex = MemberIndex + 1;
} while (Result != 0);
SetupDiDestroyDeviceInfoList(hDevInfo);
printf("%u\r\n", MemberIndex);
;
return 0;
}
If an enumeration parameter value is not used to select devices, set Enumerator to NULL and when Enumerator is NULL, SetupDiGetClassDevs returns devices for all PnP enumerators. You could set this parameter either be the value's globally unique identifier (GUID) or symbolic name.
For more information, you could refer to this document below.
https://learn.microsoft.com/en-us/windows/desktop/api/setupapi/nf-setupapi-setupdigetclassdevsw
Best Regards,
Baron Bi
I have a namespace extension in development and I'm stuck adding a folder programmatically from within the extension. What happens visually is a tool bar button is clicked, a file path is chosen and that file is the datasource that the virtual folders and files are created from.
What I want to happen since the load file button on the tool bar is within the namespace extensions root, is for the new virtual folder to appear automatically. In order for it to show up, I need to click the tree view or go up out of the root and back in. Then the folder is present.
I've researched this problem and normally when this occurs people are using SHChangeNotify. I tried that like the example below and using various combonations such as providing the path or pidl off the namespace extension root, the example below including the new folder that should be there, using the pidl of that path (with proper flag in SHChangeNotify) and still no dice. I also tried the SHCNE_xxx where xxx is a notify all flag.
SHChangeNotify(SHCNE_UPDATEDIR, SHCNF_IDLIST, retPidl, 0);
Right clicking the view pane and choosing refresh does not invoke the folder to appear.
In the code that gets the path of the folder, I called BindToObject of my folder then EnumObjects and Next, the breakpoints weren't hit. When I click the tree view or go up a folder and back down, all the breakpoints are hit. SHChangeNotify does not invoke the breakpoints.
The view is created using SHCreateShellFolderView in CreateViewObject like so:
if (IID_IShellView == riid) {
SFV_CREATE csfv = {0};
csfv.cbSize = sizeof(SFV_CREATE);
hr = QueryInterface(IID_PPV_ARGS(&csfv.pshf));
if (SUCCEEDED(hr)) {
hr = CShellFolderView_CreateInstance(IID_PPV_ARGS(&csfv.psfvcb));
if (SUCCEEDED(hr)) {
hr = SHCreateShellFolderView(&csfv, (IShellView**)ppv);
csfv.psfvcb->Release();
m_hWnd = hwndOwner;
}
csfv.pshf->Release();
}
}
in the ShellVolderView class, I set a bp on the notification flags and they never hit. I read that returning S_OK is needed for the SFVM_UPDATEOBJECT so I added that.
IFACEMETHODIMP CShellFolderView::MessageSFVCB(UINT uMsg, WPARAM wParam, LPARAM lParam) {
HRESULT hr = E_NOTIMPL;
switch(uMsg) {
case SFVM_GETSORTDEFAULTS:
wParam = (WPARAM)(int*)-1;
lParam = (LPARAM)(int*)Col::Size;
hr = S_OK;
break;
case SFVM_GETNOTIFY:
*((LONG*)lParam) = SHCNE_ALLEVENTS;
hr = S_OK;
break;
}
return hr;
}
*Edit: Adding suspect function. When the *ppidl is NULL, E_INVALIDARG is returned.
STDMETHODIMP CShellFolder::ParseDisplayName(HWND hwnd, IBindCtx *pbc, LPWSTR pszDisplayName, ULONG *pchEaten, PIDLIST_RELATIVE *ppidl, ULONG *pdwAttributes) {
HRESULT hr = E_INVALIDARG;
if (pszDisplayName && *ppidl) {
WCHAR szNameComponent[MAX_SIZE] = {};
PWSTR pszNext = PathFindNextComponent(pszDisplayName);
if (pszNext && *pszNext) {
// unsure of max length, can't use StringCchLength for this
size_t len = lstrlen(pszDisplayName);
size_t nextLen = 0;
StringCchLength(pszNext, MAX_SIZE, &nextLen);
hr = StringCchCopyN(szNameComponent, MAX_SIZE, pszDisplayName, len - nextLen);
}
else {
hr = StringCchCopy(szNameComponent, MAX_SIZE, pszDisplayName);
}
if (SUCCEEDED(hr)) {
PathRemoveBackslash(szNameComponent);
PIDLIST_RELATIVE pidlCurrent = NULL;
LPPIDLDATA child = m_pPidlMgr->GetSubItemFromPidl((LPCITEMIDLIST)ppidl);
hr = m_pPidlMgr->Create(&pidlCurrent, child);
if (SUCCEEDED(hr)) {
if (pszNext && *pszNext) {
IShellFolder *psf;
hr = BindToObject(pidlCurrent, pbc, IID_PPV_ARGS(&psf));
if (SUCCEEDED(hr)) {
PIDLIST_RELATIVE pidlNext = NULL;
hr = psf->ParseDisplayName(hwnd, pbc, pszNext, pchEaten, &pidlNext, pdwAttributes);
if (SUCCEEDED(hr)) {
*ppidl = ILCombine(pidlCurrent, pidlNext);
ILFree(pidlNext);
}
psf->Release();
}
ILFree(pidlCurrent);
}
else {
*ppidl = pidlCurrent;
}
}
}
}
return hr;
}
What steps should I be taking to get the folder to show up programmatically?
Try the following:
SFVM_GETNOTIFY:
begin
PDWORD(ALParam)^ := SHCNE_ALLEVENTS;
Result := S_OK;
end;
I am trying to restrict the users access to certain location (Windows, Program Files, etc.) and doing that by implementing the IFolderFilter interface.
Everything seems to be going fine up until the ShouldShow function gets called and then it all seems to fall apart. The initial folder that we pass in is that of C:\ProgramData\Company\App\Year and we see that, but the file dialog shows it hanging off Desktop which is why when I inspect the folders contained in the pidl are like C:\Users\Graham.Reeds\Desktop\Windows, C:\Users\Graham.Reeds\Desktop\Program Files, etc. This is seemingly preventing them from being matched up to the CSIDL_ variables I want to prevent the user from selecting.
I tried using SHGetPathFromIDListEx and SHGetKnownFolderIDList but VS2010 gives me the 'Identifier X is undefined' but I am including shlobj and link to Shell32.lib.
HRESULT STDMETHODCALLTYPE ShouldShow(IShellFolder* sf, LPCITEMIDLIST pidlFolder, LPCITEMIDLIST pidlItem)
{
HRESULT resultCode = S_OK;
ULONG attributes = 0UL;
if (SUCCEEDED(sf->GetAttributesOf(1, &pidlItem, &attributes)))
{
char szPath[_MAX_PATH];
BOOL f = SHGetPathFromIDList(pidlItem, szPath);
// BOOL f = SHGetPathFromIDListEx(pidlItem, szPath, _MAX_PATH, 0);
// FOLDERID_Windows / CSIDL_WINDOWS
PIDLIST_ABSOLUTE pidl;
// if (SUCCEEDED(SHGetKnownFolderIDList(FOLDERID_Windows, 0, 0, &pidl)))
if (SUCCEEDED(SHGetFolderLocation(0, CSIDL_WINDOWS, 0, 0, &pidl)))
{
HRESULT hres = sf->CompareIDs(0, pidlItem, pidl);
if ((short)HRESULT_CODE(hres) == 0)
{
resultCode = S_FALSE;
}
ILFree(pidl);
}
}
return resultCode;
}
What I need to do is to be able to prevent certain folders from appearing in the browse folder. How can I do that?
SHGetPathFromIDList requires an absolute PIDL (one starting from the Desktop at the top of the namespace heirarchy and working down), but you are passing it pidlItem which is a single child PIDL (pidlItem is relative to pidlFolder, not the desktop).
Instead of using SHGetPathFromIDList you can use IShellFolder::GetDisplayNameOf with the SHGDN_FORPARSING flag to get the full path of the item (e.g. sf->GetDisplayNameOf(pidlItem, ...)).
The same problem is happening with your CompareIDs call. IShellFolder::CompareIDs requires two PIDLs that are both relative to the specified folder, but you are trying to call it with one relative PIDL and one absolute one.
One solution would be to create an absolute PIDL out of pidlFolder and pidlItem using ILCombine and pass that to CompareIDs, but you would need to use the desktop folder to do the comparison (you obtain this from SHGetDesktopFolder) rather than using sf.
You are comparing apples (folder-relative PIDLs) to oranges (absolute PIDLs). You need to convert the relative PIDL to an absolute PIDL before you can compare it to another item located in a different folder of the Shell namespace.
Try something more like this:
bool ArePIDLsTheSameItem(PCIDLIST_ABSOLUTE pidl1, PCIDLIST_ABSOLUTE pidl2)
{
if (ILIsEqual(pidl1, pidl2))
return true;
IShellFolder *sfDesktop = NULL;
if (SHGetDesktopFolder(&sfDesktop) == S_OK)
{
HRESULT hres = sfDesktop->CompareIDs(0, pidl1, pidl2);
sfDesktop->Release();
if (HRESULT_CODE(hres) == 0)
return true;
}
return false;
}
HRESULT STDMETHODCALLTYPE ShouldShow(IShellFolder* sf, LPCITEMIDLIST pidlFolder, LPCITEMIDLIST pidlItem)
{
HRESULT resultCode = S_OK;
PIDLIST_ABSOLUTE pidlWindows = NULL;
//if (SUCCEEDED(SHGetKnownFolderIDList(FOLDERID_Windows, 0, 0, &pidlWindows)))
if (SUCCEEDED(SHGetFolderLocation(0, CSIDL_WINDOWS, 0, 0, &pidlWindows)))
{
if (ArePIDLsTheSameItem(pidlWindows, pidlFolder))
{
// ignore anything inside the Windows folder itself...
resultCode = S_FALSE;
}
else if (ILIsParent(pidlWindows, pidlFolder, FALSE))
{
// ignore anything inside a subfolder of the Windows folder...
resultCode = S_FALSE;
}
else
{
PIDLIST_ABSOLUTE pidlFolderAndItem = ILCombine(pidlFolder, pidlItem);
if (pidlFolderAndItem)
{
if (ArePIDLsTheSameItem(pidlFolderAndItem, pidlWindows))
{
// ignore the Windows folder itself...
resultCode = S_FALSE;
}
ILFree(pidlFolderAndItem);
}
}
ILFree(pidlWindows);
}
return resultCode;
}
Why would GetPath always return E_FAIL when querying FOLDERID_ControlPanelFolder? Other FOLDERIDs actually do work:
HRESULT hr = S_OK;
*path = '\0';
LPWSTR pwcPath = NULL;
CoInitialize(NULL);
IKnownFolderManager *pFolderManager = NULL;
if ((hr = CoCreateInstance(__uuidof(KnownFolderManager), NULL, CLSCTX_INPROC_SERVER, __uuidof(IKnownFolderManager), (LPVOID *)&pFolderManager)) == S_OK)
{
IKnownFolder *pControlPanelFolder = NULL;
if ((hr = pFolderManager->GetFolder(FOLDERID_ControlPanelFolder, &pControlPanelFolder)) == S_OK)
{
hr = pControlPanelFolder->GetPath(0, &pwcPath);
if (hr == S_OK && pwcPath)
{
int nSize = wcslen(pwcPath);
WideCharToMultiByte(CP_ACP, 0, pwcPath, nSize, path, nSize+2, NULL, NULL);
path[nSize] = '\0';
CoTaskMemFree(pwcPath);
}
pControlPanelFolder->Release();
pControlPanelFolder = NULL;
}
pFolderManager->Release();
pFolderManager = NULL;
}
CoUninitialize();
(Yes, I stumbled upon this question but I don't have need for all that enumeration stuff.)
The Control Panel has no directory path because it does not exist on the disc. You can get its PIDL, and even a Desktop Absolute Parsing "display name" (via GetShellItem and GetDisplayName), but not a directory path.
Reason why I needed the path was that I wanted to open the controp panel with a ShellExecute "open". I now execute the control panel program directly, with the benefit of being able to select the desired applet right away (in this case "Sound"). I hope it's not too pretentious that I post this as answer:
char controlpanelpath[2000];
UINT controlpanelpathbuffersize = sizeof(controlpanelpath);
int actualcontrolpanelpathsize;
if (actualcontrolpanelpathsize = GetSystemDirectory(controlpanelpath, controlpanelpathbuffersize))
{
char *parameters = "\\control.exe mmsys.cpl,,0";
if (actualcontrolpanelpathsize + strlen(parameters) < controlpanelpathbuffersize)
{
strcat(controlpanelpath, parameters);
WinExec(controlpanelpath, SW_NORMAL);
}
}