Search code examples
.netwindowsfilesystemwatcher

how can I tell when the user has finished editing a file in Paint.NET?


I want to write a screenshot app that grabs a screen shot, saves it to a file, opens Paint.NET with that file, then uploads the edited file to a service when the user is finished editing the file in Paint.NET.

I have the other stuff sort of covered. How can I detect when the image is finished being edited in Paint.NET?

I'm using .NET 3.5, C#.

I can use a FileSystemWatcher to detect changes in a specific file. But the first time a file changes does not necessarily indicate that Paint.NET is finished. I can wait for Paint.NET to exit - I suppose by examining the list of processes in Windows and detecting when Paint.NET is no longer there. But a user may finish editing a file without actually closing Paint.NET.

If I have to tell the user to close Paint.NET, to signal that the file is ready for upload, I guess I could do that. But I'm hoping to avoid that sort of extra requirement.

If Paint.NET holds a file open for read while it is being edited, then I suppose I could try watching for that. But how? Maybe by polling, trying to open the file with FileShare.None.

Is there a better way?


EDIT - no, Paint.NET does not keep the file open, not even for reading. I can open an image file no problem with FileShare.None, even while it is being displayed/edited by Paint.NET. So that idea won't work.


Does Paint.NET have a Remoting interface, where I can interrogate it for which files it has open? that would suit my purposes.


Solution

  • After fiddling with this a little bit, I think UI Automation will satisfy. Using the System.Windows.Automation classes that were new in .NET 3.0, I can inquire about the contents of other Windows on the machine, and I can find a windows for a given process ID. So it's a matter of

    • finding the PaintDotNet.exe process via a search in System.Diagnostics.Process.GetProcesses
    • obtaining the AutomationElement for the main window of the PaintDotNet app
    • checking the "name" property of that Window.
    • when it changes from "myfile.jpg" to something else, then I know Paint.NET has stopped editing the file.
    • if I get a ElementNotAvailableException , that means Paint.NET has exited.

    I haven't tested this very much, but, this code seems to work for my purposes:

    public void Run()
    {
        var shortFileName = Path.GetFileName(_filename);
        System.Console.WriteLine("Waiting for PDN to finish with {0}", shortFileName);
    
        var s= from p in Process.GetProcesses()
            where p.ProcessName.Contains("PaintDotNet.exe")
            select p;
    
        if (s.Count()==0)
        {
            System.Console.WriteLine("PDN is not running.");
            return;
        }
    
        var process = s.First();
        var window = AutomationElement.RootElement.FindChildByProcessId(process.Id);
    
        string name =
            window.GetCurrentPropertyValue(AutomationElement.NameProperty) as string;
    
        if (!name.StartsWith(shortFileName))
        {
            System.Console.WriteLine("PDN appears to NOT be editing the file.");
        }
        else
        {
            try
            {
                int cycles = 0;
                do
                {
                    System.Threading.Thread.Sleep(800);
                    name = window.GetCurrentPropertyValue(AutomationElement.NameProperty) as string;
                    if (!name.StartsWith(shortFileName)) break;
                    cycles++;
                    System.Console.Write(".");
                } while (cycles < 24);
    
                if (!name.StartsWith(shortFileName))
                    System.Console.WriteLine("PDN is done.");
                else
                    System.Console.WriteLine("Timeout.");
            }
            catch (ElementNotAvailableException)
            {
                System.Console.WriteLine("PDN has exited.");
            }
        }
    }
    

    The FindChildByProcessId method is an extension method from this blog post. It looks like this:

    public static class AutomationExtensions
    {
        public static AutomationElement FindChildByProcessId(this AutomationElement element, int pid)
        {
            var cond = new PropertyCondition(AutomationElement.ProcessIdProperty, pid);
            var result = element.FindChildByCondition(cond);
            return result;
        }
    
        public static AutomationElement FindChildByCondition(this AutomationElement element, Condition cond)
        {
            var result = element.FindFirst(TreeScope.Children, cond);
            return result;
        }
    }
    

    This seems a little unorthodox, but, it just works. The trickiest part is the timing - you need to wait long enough to let the app start up, which can be 1 second or it could be 7 seconds. Then try to attach to it with the UIAutomation classes.