It seems like calling SHChangeNotify in Windows does not call an update to the items in the QuickAccess pane (or any custom-namespace folders found on the left side of Explorer). Seems like expanding the tree on the left side to the folder works alright, as well as anything in the main view on the right side.
We are calling the SHChangeNotify from a c# WPF app, though the SHChangeNotify seems to call into our DLL hook in explorer just fine for anything in the right view. This will eventually call into a named-pipe that will hook back into our c# code to call an update to the file or folder's icon.
This is what we are calling from c#:
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
private static extern void SHChangeNotify(
int wEventId,
uint uFlags,
IntPtr dwItem1,
IntPtr dwItem2);
var ptr = Marshal.StringToHGlobalUni(fullPath);
SHChangeNotify((int)SHCNE.SHCNE_UPDATEITEM, (int)(SHCNF.SHCNF_PATHW | SHCNF.SHCNF_FLUSHNOWAIT), ptr, IntPtr.Zero);
Marshal.FreeHGlobal(ptr);
We can assume that all consts and enums are defined correctly.
This is what the icons look like:
Note that the gray icon is the default icon. The green icon in the main window was triggered by calling the function above with the path C:\Users\Test User\Pocket Test. I would think that this should trigger a refresh for both folders.
I've also tried replacing SHCNF_FLUSHNOWAIT with SHCNF_FLUSH. I'm at a loss on how to procede here. Any any ideas on how to force-update the folders on that left pane in Explorer?
The Quick Access virtual folder path, as a string, is shell:::{679f85cb-0220-4080-b29b-5540cc05aab6}, (this guid name is CLSID_HomeFolder).
So, you can force a refresh of all items under this virtual folder with a call to:
SHChangeNotify(SHCNE_UPDATEDIR, SHCNF_PATHW, L"shell:::{679f85cb-0220-4080-b29b-5540cc05aab6}", NULL);
If you want to refresh only a specific set of children, just get the PIDL or the path of these items and call SHChangeNotify(SHCNE_UPDATEITEM, ...) on each.
Related
I'm trying to make a program to switch my taskbar to match my virtual desktop in Windows 11. For instance, I'd have a Desktop named "3d Modeling" where I'd have Blender, Meshmixer, etc., pinned to the taskbar, but when I switch to the "3d Printing" Desktop then the taskbar would have SuperSlicer, Lychee, etc., pinned instead.
I have most of this working using an XML layout file, and using some powershell and the VirtualDesktop module to import the layout that matches the current desktop. The layout will be imported, but the taskbar won't reflect that change unless explorer.exe is restarted. But, when I restart Explorer, the program starts throwing a bunch of errors, I'm assuming because the module is unable to reconnect to the new Explorer process. The program works as expected other than the taskbar layouts not visually displaying (desktop switch is detected, new layout is selected properly, and layout is imported successfully) if Explorer is not restarted.
I would much rather not have to fully restart Explorer since that causes a bunch of stuff to disappear from the screen. I have gone down the rabbit hole a bit trying to get this to work using some C#, specifically following this thread and poking through the code for the AdaptiveTaskbar module. Both of those are older than Win11, so I'm thinking something may have changed since then. So far I haven't managed to get it to refresh the taskbar at all, and I'm not experienced enough with the Windows API to know where to look next.
So my question is: is there a way to do this in Win11? It doesn't necessarily have to be a C# or Powershell solution. Here's my most recent code, the relevant parts taken from AdaptiveTaskbar in case there's maybe a small thing I'm missing. I have also tried it using SendMessageTimeout.
Import-Module VirtualDesktop
Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition #"
[DllImport("User32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool SendNotifyMessage(IntPtr hWnd, uint Msg, IntPtr wParam, string lParam);
"#
$lastDesktopIndex = -1
$currentDesktopIndex = 0
$currentDesktopName = ''
while ($True) {
$currentDesktopIndex = Get-DesktopIndex -Desktop $(Get-CurrentDesktop)
$currentDesktopName = Get-DesktopName -Desktop $(Get-CurrentDesktop)
if ($lastDesktopIndex -ne $currentDesktopIndex) {
Import-StartLayout -LayoutPath "$PSScriptRoot\taskbars\$currentDesktopName.xml" -MountPath "C:\"
Write-Output "$currentDesktopIndex : $currentDesktopName : $PSScriptRoot\taskbars\$currentDesktopName.xml"
# Stop-Process -name explorer
# Start-Process -FilePath "explorer.exe"
[void] ([Win32.Nativemethods]::SendNotifymessage([IntPtr]0xffff, 0x1a, [IntPtr]::Zero, "TraySettings"))
}
$lastDesktopIndex = $currentDesktopIndex
#Start-Sleep -Seconds 1
Read-Host -Prompt "Press any key to continue"
}
I'm currently creating a WPF application, and like to add as a small side feature the ability to clear the windows file explorer history.
If one were to manually do this operation, it is possible via the file menu within a file explorer window,
as shown here.
My goal is pretty much to programmatically execute the same action as this button does, but I've been unable to find what executable or user32.dll method is behind this operation (if it exists), and been also unsuccessful on finding the full logic behind it (namely, finding what folder and files it targets), to replicate it.
Can you help me?
As the comment by dxiv suggested, you can achieve this via the following:
enum ShellAddToRecentDocsFlags
{
Pidl = 0x001,
Path = 0x002,
PathW = 0x003
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern void SHAddToRecentDocs(ShellAddToRecentDocsFlags flag, string path);
// How To Clear Everything
SHAddToRecentDocs(ShellAddToRecentDocsFlags.Pidl, null);
My goal is to write a C# code that will open a Windows Explorer window, with a particular file selected. If such window is already open, I want to bring it to front. I have tried two options.
First, I start by explicitly calling explorer.exe:
arg = "/select, " + pathToFile;
Process.Start("explorer.exe", arg);
This opens and selects a window fine, but the problem is that it will always open a new window, even if one exists. So I tried this:
Process.Start(pathToDir);
This either opens a new window or focuses an old one, but gives me no option to select a file.
What can I do? I looked at explorer's arguments and I don't see anything I can use. A last-resort option I can come up with is to get the list of already open windows and use some WINAPI-level code to handle it, but that seems like an overkill.
I don't know if it's possible using process start, but the following code opens the Windows explorer on the containing folder only if needed (if the folder is already open, or selected on another file, it's reused) and selects the desired file.
It's using p/invoke interop code on the SHOpenFolderAndSelectItems function:
public static void OpenFolderAndSelectFile(string filePath)
{
if (filePath == null)
throw new ArgumentNullException("filePath");
IntPtr pidl = ILCreateFromPathW(filePath);
SHOpenFolderAndSelectItems(pidl, 0, IntPtr.Zero, 0);
ILFree(pidl);
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr ILCreateFromPathW(string pszPath);
[DllImport("shell32.dll")]
private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags);
[DllImport("shell32.dll")]
private static extern void ILFree(IntPtr pidl);
I am using Tamas Szekeres builds of GDAL including the C# bindings in a desktop GIS application using C# and .net 4.0
I am including the entire GDAL distribution in a sub-directory of my executable with the following folder structure:
\Plugins\GDAL
\Plugins\GDAL\gdal
\Plugins\GDAL\gdal-data
\Plugins\GDAL\proj
We are using EPSG:4326, and the software is built using 32-bit target since the GDAL C# API is using p/invoke to the 32-bit libraries (could try 64 bit since Tamas provides these, haven't gotten around to it yet).
When I run my application I get the following error
This error typically happens when software tries to access a device that is no longer attached, such as a removable drive. It is not possible to "catch" this exception because it pops up a system dialog.
After dismissing the dialog using any of the buttons, the software continues to execute as designed.
The error occurs the first time I call the following method
OSGeo.OSR.CoordinateTransformation.TransformPoint(double[] inout);
The strange stuff:
The error occurs on one, and only one computer (so far)
I've run this software in several other computers both 32 and 64 bit without problems
The error does not ocurr on the first run after compiling the GDAL shim library I am using, it only occurrs on each subsequent run
it happens regardless of release, or debug builds
it happens regardless of whether the debugger is attached or not
it happens regardless of whether I turn on or off Gdal.UseExceptions or Osr.UseExceptions();
disabling removable drives causes the bug to disappear. This is not what I consider a real solution as I will not be able to ask a customer to do this.
I have tried the following:
catching the error
changing GDAL directories and environment settings
changing computers and operating systems: this worked
used SysInternals ProcMon to trace what files are being opened with no luck, they all appear to be files that exist
I re-built the computer in question when the hard drive failed, to no avail.
"cleaning" the registry using CCleaner
files in GDAL Directory are unchanged on execution
Assumptions
Error is happening in unmanaged code
During GDAL initialization, some path is referring to a drive on the computer that is no longer attached.
I am also working on the assumption this is limited to a computer configuration error
Configuration
Windows 7 Pro
Intel Core i7 920 # 2,67GHz
12.0 GB RAM
64-bit OS
Drive C: 120 GB SSD with OS, development (Visual Studio 10), etc
Drive D: 1 TB WD 10,000k with data, not being accessed for data.
The Question
I either need a direction to trap the error, or a tool or technique that will allow me to figure out what is causing it. I don't want to release the software with the possibility that some systems will have this behaviour.
I have no experience with this library, but perhaps some fresh eyes might give you a brainwave...
Firstly, WELL WRITTEN QUESTION! Obviously this problem really has you stumped...
Your note about the error not occurring after a rebuild screams out: Does this library generate some kind of state file, in its binary directory, after it runs?
If so, it is possible that it is saving incorrect path information into that 'configuration' file, in a misguided attempt to accelerate its next start-up.
Perhaps scan this directory for changes between a 'fresh build' and 'first run'?
At very least you might find a file you can clean up on shut-down to avoid this alert...
HTH
Maybe you can try this:
Run diskmgmt.msc
Change the driveletter for Disk 2 (right click) if my assumption that Disk 2 is a Removable Disk is true
Run your application
If this removes the error, something in the application is referring to the old driveletter
It could be in the p/invoked libs
Maybe see: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=46501 It talks about gcc somehow compiling a driveletter into a binary
+1 Great question, but It is not possible to "catch"
Its one of these awful solutions that will turn up on DailyWTF in 5 years. But for now it is stored here http://www.pinvoke.net/default.aspx/user32.senddlgitemmessage
using Microsoft.VisualBasic; //this reference is for the Constants.vbNo;
public partial class Form1 : Form
{
[DllImport("user32.dll")]
static extern IntPtr SendDlgItemMessage(IntPtr hDlg, int nIDDlgItem, uint Msg, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetActiveWindow(IntPtr hWnd);
// For Windows Mobile, replace user32.dll with coredll.dll
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
// Find window by Caption only. Note you must pass IntPtr.Zero as the first parameter.
[DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetDlgItemText(IntPtr hDlg, int nIDDlgItem,[Out] StringBuilder lpString, int nMaxCount);
public void ClickSaveBoxNoButton()
{
//In this example, we've opened a Notepad instance, entered some text, and clicked the 'X' to close Notepad.
//Of course we received the 'Do you want to save...' message, and we left it sitting there. Now on to the code...
//
//Note: this example also uses API calls to FindWindow, GetDlgItemText, and SetActiveWindow.
// You'll have to find those separately.
//Find the dialog box (no need to find a "parent" first)
//classname is #32770 (dialog box), dialog box title is Notepad
IntPtr theDialogBoxHandle; // = null;
string theDialogBoxClassName = "#32770";
string theDialogBoxTitle = "Notepad";
int theDialogItemId = Convert.ToInt32("0xFFFF", 16);
StringBuilder theDialogTextHolder = new StringBuilder(1000);
//hardcoding capacity - represents maximum text length
string theDialogText = string.Empty;
string textToLookFor = "Do you want to save changes to Untitled?";
bool isChangeMessage = false;
IntPtr theNoButtonHandle; // = null;
int theNoButtonItemId = (int)Constants.vbNo;
//actual Item ID = 7
uint theClickMessage = Convert.ToUInt32("0x00F5", 16);
//= BM_CLICK value
uint wParam = 0;
uint lParam = 0;
//Get a dialog box described by the specified info
theDialogBoxHandle = FindWindow(theDialogBoxClassName, theDialogBoxTitle);
//a matching dialog box was found, so continue
if (theDialogBoxHandle != IntPtr.Zero)
{
//then get the text
GetDlgItemText(theDialogBoxHandle, theDialogItemId, theDialogTextHolder, theDialogTextHolder.Capacity);
theDialogText = theDialogTextHolder.ToString();
}
//Make sure it's the right dialog box, based on the text we got.
isChangeMessage = Regex.IsMatch(theDialogText, textToLookFor);
if ((isChangeMessage))
{
//Set the dialog box as the active window
SetActiveWindow(theDialogBoxHandle);
//And, click the No button
SendDlgItemMessage(theDialogBoxHandle, theNoButtonItemId, theClickMessage, (System.UIntPtr)wParam, (System.IntPtr)lParam);
}
}
It turns out there was no way to definitely answer this question.
I ended up "solving" the problem by figuring out that there was some hardware registered on the system that wasn't present. It is still a mystery to me why, after several years, only GDAL managed to provoke this bug.
I will put the inability to catch this exception down to the idiosyncrasies involved with p/invoke and the hardware error thrown at a very low level on the system.
You could add custom error handlers to gdal. This may help:
Link
http://trac.osgeo.org/gdal/ticket/2895
I'm building a small GUI-Test automation tool in C# for a application.
One of the functions in the test tool is to close dialogs pops up from the tested application.
The trouble that I have is to find the button to click on without giving the full class name. I have used the FindWindowEx method to get the dialog box and the button that I want to click on. I know the Caption of the button, but the trouble is that I also need to specify the class name for the button. The class name is not always the same, but it looks something like this: "WindowsForms10.BUTTON.app.0.3ce0bb8". For instance is the part in the end "3ce0bb8" different if you start the application locally or via click-once.
So, my question is: How can I find the button with just specifying the first part (that is always the same) of the class like this ""WindowsForms10.BUTTON.app." Or could I solve this in some other way?
The dll import looks like this:
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string pszWindow);
My code looks something like this when trying to click the button:
private void SendDialogButtonClick(IntPtr windowHandle, ApplicationStartType applicationStartType)
{
if (applicationStartType == ApplicationStartType.Localy)
buttonClassName = "WindowsForms10.BUTTON.app.0.3ce0bb8";
else if (applicationStartType == ApplicationStartType.ClickOnce)
buttonClassName = "WindowsForms10.BUTTON.app.0.3d893c";
// Find the "&No"-button
IntPtr buttonAndNoHandle = FindWindowEx(windowHandle, IntPtr.Zero, buttonClassName, "&No");
// Send the button click event to the appropriate button found on the dialog
if (buttonAndNoHandle.ToInt64() != 0)
{
SendMessage(new HandleRef(null, buttonAndNoHandle), WM_CLICK, IntPtr.Zero, IntPtr.Zero);
}
}
Yes, that's difficult, the class names are auto-generated. You can't use FindWindowEx(), you have to iterate the controls with EnumChildWindows() and GetClassName().
You could adapt the source code for the Managed Spy tool to make all this a lot easier and cleaner.