Reputation: 23229
I have a .xlsx file that I wish to launch in Excel from C#. To do this I use the Process.start()
API with the open
verb.
This works fine except that the Excel window appears briefly and then gets hidden behind the main app.
Oddly launching a PDF (Adoboe Viewer as the default view) using exactly the same API in exactly the same section of code works fine, the PDF appears maximised and stays there. This would appear to rule out my app moving itself back to the front after Excel has launched.
Does anyone know what might be causing this?
EDIT: added code
ProcessStartInfo startInfo = new ProcessStartInfo(filename);
startInfo.WindowStyle = windowStyle; // maximized
startInfo.Verb = "open";
startInfo.ErrorDialog = false;
Process.Start(startInfo);
Upvotes: 1
Views: 3099
Reputation: 1878
With Excel 2010 I found that Evan Mulawski's solution didn't work. An exception was thrown when trying to call .WaitForInputIdle, because when you open your second (or third, or fourth) Excel spreadsheet, the Excel process you start detects the first instance of Excel, tells it to open the document and then immediately closes. That means your Process object no longer has a process to call .WaitForInputIdle on.
I solved it with the following helper class that I put together. I haven't tested this extensively with applications other than Excel, but it works nicely to focus Excel and should in theory work with any "single instance" application.
Simply call ShellHelpers.OpenFileWithFocus("C:\Full\Path\To\file.xls")
to use it.
Thanks to Evan Mulawski for the original code concept, which I built on :)
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Diagnostics;
using System.Threading;
namespace Resolv.Extensions.System.UI
{
public static class ShellHelpers
{
private const long FindExecutable_SE_ERR_FNF = 2; //The specified file was not found.
private const long FindExecutable_SE_ERR_PNF = 3; // The specified path is invalid.
private const long FindExecutable_SE_ERR_ACCESSDENIED = 5; // The specified file cannot be accessed.
private const long FindExecutable_SE_ERR_OOM = 8; // The system is out of memory or resources.
private const long FindExecutable_SE_ERR_NOASSOC = 31; // There is no association for the specified file type with an executable file.
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("shell32.dll", EntryPoint = "FindExecutable")]
private static extern long FindExecutableA(string lpFile, string lpDirectory, StringBuilder lpResult);
private class ProcessInfo
{
public string ProcessPath { get; set; }
public Process Process { get; set; }
}
/// <summary>
/// Opens the specified file in the default associated program, and sets focus to
/// the opened program window. The focus setting is required for applications,
/// such as Microsoft Excel, which re-use a single process and may not set focus
/// when opening a second (or third etc) file.
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public static bool OpenFileWithFocus(string filePath)
{
string exePath;
if (!TryFindExecutable(filePath, out exePath))
{
return false;
}
Process viewerProcess = new Process();
viewerProcess.StartInfo.FileName = exePath;
viewerProcess.StartInfo.Verb = "open";
viewerProcess.StartInfo.ErrorDialog = true;
viewerProcess.StartInfo.Arguments = filePath;
ProcessInfo info = new ProcessInfo() {Process = viewerProcess, ProcessPath = exePath};
viewerProcess.Start();
ThreadPool.QueueUserWorkItem(SetWindowFocusForProcess, info);
return true;
}
/// <summary>
/// To be run in a background thread: Attempts to set focus to the
/// specified process, or another process from the same executable.
/// </summary>
/// <param name="processInfo"></param>
private static void SetWindowFocusForProcess(object processInfo)
{
ProcessInfo windowProcessInfo = processInfo as ProcessInfo;
if (windowProcessInfo == null)
return;
int tryCount = 0;
Process process = windowProcessInfo.Process;
while (tryCount < 5)
{
try
{
process.WaitForInputIdle(1000); // This may throw an exception if the process we started is no longer running
IntPtr hWnd = process.MainWindowHandle;
if (SetForegroundWindow(hWnd))
{
break;
}
}
catch
{
// Applications that ensure a single process will have closed the
// process we opened earlier and handed the command line arguments to
// another process. We should find the "single" process for the
// requested application.
if (process == windowProcessInfo.Process)
{
Process newProcess = GetFirstProcessByPath(windowProcessInfo.ProcessPath);
if (newProcess != null)
process = newProcess;
}
}
tryCount++;
}
}
/// <summary>
/// Gets the first process (running instance) of the specified
/// executable.
/// </summary>
/// <param name="executablePath"></param>
/// <returns>A Process object, if any instances of the executable could be found running - otherwise NULL</returns>
public static Process GetFirstProcessByPath(string executablePath)
{
Process result;
if (TryGetFirstProcessByPath(executablePath, out result))
return result;
return null;
}
/// <summary>
/// Gets the first process (running instance) of the specified
/// executable
/// </summary>
/// <param name="executablePath"></param>
/// <param name="process"></param>
/// <returns>TRUE if an instance of the specified executable could be found running</returns>
public static bool TryGetFirstProcessByPath(string executablePath, out Process process)
{
Process[] processes = Process.GetProcesses();
foreach (var item in processes)
{
if (string.Equals(item.MainModule.FileName, executablePath, StringComparison.InvariantCultureIgnoreCase))
{
process = item;
return true;
}
}
process = null;
return false;
}
/// <summary>
/// Return system default application for specified file
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public static string FindExecutable(string filePath)
{
string result;
TryFindExecutable(filePath, out result, raiseExceptions: true);
return result;
}
/// <summary>
/// Attempts to find the associated application for the specified file
/// </summary>
/// <param name="filePath"></param>
/// <param name="executablePath"></param>
/// <returns>TRUE if an executable was associated with the specified file. FALSE
/// if there was an error, or an association could not be found</returns>
public static bool TryFindExecutable(string filePath, out string executablePath)
{
return TryFindExecutable(filePath, out executablePath, raiseExceptions: false);
}
/// <summary>
/// Attempts to find the associated application for the specified file. Throws
/// exceptions if the file could not be opened or does not exist, but returns
/// FALSE when there is no application associated with the file type.
/// </summary>
/// <param name="filePath"></param>
/// <param name="executablePath"></param>
/// <param name="raiseExceptions"></param>
/// <returns></returns>
public static bool TryFindExecutable(string filePath, out string executablePath, bool raiseExceptions)
{
// Anytime a C++ API returns a zero-terminated string pointer as a parameter
// you need to use a StringBuilder to accept the value instead of a
// System.String object.
StringBuilder oResultBuffer = new StringBuilder(1024);
long lResult = 0;
lResult = FindExecutableA(filePath, string.Empty, oResultBuffer);
if (lResult >= 32)
{
executablePath = oResultBuffer.ToString();
return true;
}
switch (lResult)
{
case FindExecutable_SE_ERR_NOASSOC:
executablePath = "";
return false;
case FindExecutable_SE_ERR_FNF:
case FindExecutable_SE_ERR_PNF:
if (raiseExceptions)
{
throw new Exception(String.Format("File \"{0}\" not found. Cannot determine associated application.", filePath));
}
break;
case FindExecutable_SE_ERR_ACCESSDENIED:
if (raiseExceptions)
{
throw new Exception(String.Format("Access denied to file \"{0}\". Cannot determine associated application.", filePath));
}
break;
default:
if (raiseExceptions)
{
throw new Exception(String.Format("Error while finding associated application for \"{0}\". FindExecutableA returned {1}", filePath, lResult));
}
break;
}
executablePath = null;
return false;
}
}
}
As a bonus, my helper class has a couple of other useful methods (such as finding running instances of a particular executable, or determining whether a particular file has an associated application).
UPDATE: Actually, it seems Excel 2010 does launch separate processes when you call Process.Start on the excel executable, meaning my code that finds other instances of the same .exe isn't required for Excel and never runs.
When I started using Evan Mulawski's solution I was calling Process.Start on the CSV I was trying to open, which meant that excel was maintaining a single process (and therefore causing exceptions).
Possibly running the excel exe (after somehow figuring out where it lives on the PC) is what Evan was suggesting in his answer and I may have misunderstood.
Anyhow, as an added bonus, running the Excel exe (rather than calling Process.Start on a CSV or XLS file) means you get separate Excel instances, which also means you get separate Excel windows and can put them on different monitors or view them side by side on the same screen. Usually when you double click Excel files (in Excel versions prior to 2013) you end up with them all opened inside the same Excel instance/window and can't tile them or put them on separate monitors because Excel versions prior to 2013 are still Single Document Interface (yuck!)
Cheers
Daniel
Upvotes: 1
Reputation: 23229
Answering my own question.
Turns out this was a DevExpress bug/feature. It is something with their AlertControl that takes focus back when you click it.
DevExpress in the usual impressively prompt fashion have fixed the problem already. See this item
Upvotes: 1
Reputation: 2408
I would use SetForeground on the Excel window.
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
To get the handle to Excel you have to the following:
Process p = Process.Start(startInfo);
System.IntPtr hWnd = p.Handle;
Upvotes: 3
Reputation: 55334
Start Excel:
Process myProcess = new Process();
myProcess.StartInfo.FileName = "Excel"; //or similar
myProcess.Start();
IntPtr hWnd = myProcess.Handle;
SetFocus(new HandleRef(null, hWnd));
Import the SetFocus function from user32.dll:
[DllImport("user32.dll", CharSet=CharSet.Auto,ExactSpelling=true)]
public static extern IntPtr SetFocus(HandleRef hWnd);
Place the import outside of your function. You may have to sleep the main thread to wait for Excel to start.
Edit:
System.Diagnostics.Process myProcess = new
System.Diagnostics.Process();
myProcess.StartInfo.FileName = "Excel"; //or similar
myProcess.Start();
myProcess.WaitForInputIdle(2000);
IntPtr hWnd = myProcess.MainWindowHandle;
bool p = SetForegroundWindow(hWnd);
if(!p)
{//could not set focus}
Imports:
[DllImport("user32.dll", CharSet=CharSet.Auto,SetLastError=true)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll", CharSet=CharSet.Auto,SetLastError=true)]
public static extern IntPtr SetFocus(IntPtr hWnd);
This will wait until the application starts before attempting to set focus to it.
Upvotes: 4