简体   繁体   中英

Activate a hidden wpf application when trying to run a second instance

I am working on a wpf application where instead of exiting the app when user closes the button I am minimizing it to the tray(similar to google talk).

    void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        e.Cancel = true;

        this.Hide();
    }

What I need is if user forgets that there is an instance of the app and tries to open a new instance I have to shut down the second instance and set my application as the foreground app. If the app is in minimized state (not hidden) I am able to do this. I am using the following code

      protected override void OnStartup(StartupEventArgs e)
           {

            Process currentProcess = Process.GetCurrentProcess();


            var runningProcess = (from process in Process.GetProcesses()
                              where
                              process.Id != currentProcess.Id &&
                              process.ProcessName.Equals(
                              currentProcess.ProcessName,
                              StringComparison.Ordinal)
                              select process).FirstOrDefault();
            if (runningProcess != null)
                {
                    Application.Current.Shutdown();

                    ShowWindow(runningProcess.MainWindowHandle, 5);

                    ShowWindow(runningProcess.MainWindowHandle, 3);
                }

           }

      [DllImport("user32.dll")]
      private static extern Boolean ShowWindow(IntPtr hWnd, Int32 nCmdShow);

When the app is minimized it has some unique value for MainWindowHandle. When I hide the app, the MainWindowHandle of runningProcess is showing as 0. I think this is why my application is not opening when it is in hidden state, but don't know how to fix it.

Tell me if I need to post more code or clarify anything. Thank you in advance.

When I hide the app, the MainWindowHandle of runningProcess is showing as 0

You're right. If a process doesn't have a graphical interface associated with it (hidden/ minimized) then the MainWindowHandle value is zero.

As workaround, you could try getting the HANDLE for the hidden window by enumerating all open windows using EnumDesktopWindows function and compare its process id with the hidden/ minimized windows's process id.

Update

The WPF's WIN32 window has a bit different behavior than the standard WIN32 window. It has the class name composed of the word HwndWrapper , the name of AppDomain it was created in, and a unique random Guid (which changes on every launch), eg, HwndWrapper[WpfApp.exe;;4d426cdc-31cf-4e4c-88c7-ede846ab6d44] .

Update 2

When WPF's window is hidden by using the Hide() method, it internally calls UpdateVisibilityProperty(Visibility.Hidden) , which in turns set the internal visibility flag for UIElement to false. When we call the Show() method of WPF Window , UpdateVisibilityProperty(Visibility.Visible) is called, and the internal visibility flag for UIElement is toggled.

When we show the WPF window using the ShowWindow() , the UpdateVisibilityProperty() method is not trigerred, thus the internal visibility flag does not get reversed (which causes the window to be displayed with black backround).

By looking at the WPF Window internal implementation, the only way to toggle the internal visiblity flag, without calling the Show() or Hide() method, is by sending a WM_SHOWWINDOW message.

const int GWL_EXSTYLE = (-20);
const uint WS_EX_APPWINDOW = 0x40000;

const uint WM_SHOWWINDOW = 0x0018;
const int SW_PARENTOPENING = 3;

[DllImport("user32.dll")]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll")]
private static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumWindowsProc ewp, int lParam);

[DllImport("user32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

[DllImport("user32.dll")]
private static extern uint GetWindowTextLength(IntPtr hWnd);

[DllImport("user32.dll")]
private static extern uint GetWindowText(IntPtr hWnd, StringBuilder lpString, uint nMaxCount);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern bool GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);

delegate bool EnumWindowsProc(IntPtr hWnd, int lParam);

static bool IsApplicationWindow(IntPtr hWnd) {
  return (GetWindowLong(hWnd, GWL_EXSTYLE) & WS_EX_APPWINDOW) != 0;
}

static IntPtr GetWindowHandle(int pid, string title) {
  var result = IntPtr.Zero;

  EnumWindowsProc enumerateHandle = delegate(IntPtr hWnd, int lParam)
  {
    int id;
    GetWindowThreadProcessId(hWnd, out id);        

    if (pid == id) {
      var clsName = new StringBuilder(256);
      var hasClass = GetClassName(hWnd, clsName, 256);
      if (hasClass) {

        var maxLength = (int)GetWindowTextLength(hWnd);
        var builder = new StringBuilder(maxLength + 1);
        GetWindowText(hWnd, builder, (uint)builder.Capacity);

        var text = builder.ToString(); 
        var className = clsName.ToString();

        // There could be multiple handle associated with our pid, 
        // so we return the first handle that satisfy:
        // 1) the handle title/ caption matches our window title,
        // 2) the window class name starts with HwndWrapper (WPF specific)
        // 3) the window has WS_EX_APPWINDOW style

        if (title == text && className.StartsWith("HwndWrapper") && IsApplicationWindow(hWnd))
        {
          result = hWnd;
          return false;
        }
      }
    }
    return true;
  };

  EnumDesktopWindows(IntPtr.Zero, enumerateHandle, 0);

  return result;
}

Usage Example

...
if (runningProcess.MainWindowHandle == IntPtr.Zero) {
  var handle = GetWindowHandle(runningProcess.Id, runningProcess.MainWindowTitle);
  if (handle != IntPtr.Zero) {
    // show window
    ShowWindow(handle, 5);
    // send WM_SHOWWINDOW message to toggle the visibility flag
    SendMessage(handle, WM_SHOWWINDOW, IntPtr.Zero, new IntPtr(SW_PARENTOPENING));
  }
}
...

Thanks IronGeek, This is great. I'm just learning c# and struggled for a while trying to get this to work. Also I cant 'add comment' as insufficient reputation here so hence this post. I'm using WPF .Net 5.0. I searched around to implement this, so for other newbies they will also need something like the following in their program to receive the message (sorry, not sure what page I copied this from, so many of them (individuals will need to make their own Mainwindow_Loaded event handler).

    private void Mainwindow_Loaded_Event(object sender, RoutedEventArgs e)
    {
        hwndSource = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
        hwndSource.AddHook(new HwndSourceHook(WndProc));
    }

    private  IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (msg == WM_SHOWWINDOW)
        {
            MessageBox.Show("I recieved WM_SHOWWINDOW");
            handled = true;
        }
        return IntPtr.Zero;
    }

The 'bring to front' tip you mentioned was also needed in my case, here is what is needed: (from Bring Word to Front ) put this in the declarations section:

[DllImport("user32.dll")]
static extern bool SetForegroundWindow(IntPtr hWnd);

and put the following just after the 'SendMessage(handle, WM_SHOWWINDOW, IntPtr.Zero, new IntPtr(SW_PARENTOPENING));'statement:

SetForegroundWindow(handle);

Without this, the activated window just remains hidden behind other windows and it has to be found by manually fishing around in the taskbar. So now I've finally got this going for a non-hidden window but now need to look at what is needed for a hidden one as that is my real goal.

Following on from my early post, and to quote an earlier comment from IronGeek the issue is ' If a process doesn't have a graphical interface associated with it (hidden/ minimized) then the MainWindowHandle value is zero'. Therefore any attempt to pass a hidden Window's handle is doomed as it doesn't exist.

So I have found a work-around, although it requires the target process to regularly check for the presence of a new message. Therefore this is still not ideal, but it works for me in 2021 (WPF, .Net 5.0) and doesn't need to import the user32.dll. Rather it carries out a makeshift type of Inter Process Communication (IPC) using the MainWindowTitle as a container to send a message passively. The MainWindowTitle is settable at runtime and is viewable from other processes, therefore it can be used like an inter-process variable. This is my entire solution below, note it needs to be published to a local folder to see how it runs as the point is to run multiple instances.

<Window x:Class="TitleComsTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:TitleComsTest"
    mc:Ignorable="d"
    Name="TitleComsTest" Title="TitleComsTest" Height="400" Width="600" 
    WindowStartupLocation = "CenterScreen" Closing="TitleComsTest_Closing" Visibility="Hidden">
<Grid>
    <TextBox Name ="TextBox1" HorizontalAlignment="Center" VerticalAlignment="Center" Height="230" Width="460"/>
    <Button Name="QuitButton" Content=" Really Quit " HorizontalAlignment="Center" Margin="0,0,0,30" VerticalAlignment="Bottom" Click="QuitButton_Click"/>
</Grid>

The code behind:

using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Threading;

namespace TitleComsTest
{
    public partial class MainWindow : Window
    {
        //string for MainWindowTitle when first instance and visible:
        const string my_Visible_exe_Name = "TitleComstest";
        //string for MainWindowTitle when first instance and Hidden:
        const string my_Hidden_exe_Name = "TitleComstest...";
        //string for MainWindowTitle when 2nd instance :
        const string my_exe_Name_Flag = "TitleComstest, Please-Wait";   

        bool reallyCloseThisProgram = false;
        private DispatcherTimer timer1, timer2;
        
        public MainWindow()
        {
            InitializeComponent();

            //Get an array of processes with the chosen name
            Process[] TitleComstests = Process.GetProcessesByName(my_Visible_exe_Name);
            if (TitleComstests.Length > 1)
            {   //Then this is not the first instance
                for (int i = 0; i < TitleComstests.Length; i++)
                {
                    if (TitleComstests[i].MainWindowTitle == my_Visible_exe_Name)
                    {   //The first instance is visible as the MainWindowTitle has been set to the visible name
                        Close(); //Quit - nothing to do but close the new instance
                    }
                }
                //The first instance is hidden, so set MainWindowTitle so the first instance can see it and react
                this.Title = my_exe_Name_Flag;
                this.WindowState = WindowState.Minimized; //Minimize the window to avoid having two windows shown at once
                this.Visibility = Visibility.Visible;     //The second instance needs to be visible (minimized is enough) to be seen
                StartTimerQuit(4000); //arbitrary time, needs to be longer than 2000ms which is the checking period - see StartTimerLook(2000);
            }
            else
            {
                TextBox1.Text = "This is Multi-instance demo using the 'MainWindowTitle' to send messages\r\nto the first (hidden) instance to wake it up.";
                TextBox1.Text += "\r\n\r\nThis demo requires the program be published to a local folder and \r\nnot run in the debugger.";
                TextBox1.Text += "\r\n\r\nYou can type here to mark this instance: _____________ \r\n\r\nand then hide me by clicking top right close window 'X'";
                TextBox1.Text += "\r\n\r\nOnce closed then start the program again to see the 1st instance pop up.";
                TextBox1.Text += "\r\n\r\nFinally use the 'Really Quit' button to end this demo.";
                this.Visibility = Visibility.Visible;
            }
        }

        private void StartTimerQuit(Int32 interval) //Timer to Quit setup and start
        {
            timer1 = new DispatcherTimer();   timer1.Tick += timerQuit_Tick;
            timer1.Interval = new TimeSpan(0, 0, 0, 0, interval);   timer1.Start();
        }
        private void timerQuit_Tick(object sender, EventArgs e)
        {
            reallyCloseThisProgram = true;   Close(); 
        }

        private void TitleComsTest_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if (!reallyCloseThisProgram)
            {
                e.Cancel = true;
                this.Title = my_Hidden_exe_Name; //Set the Title text to flag a hidden state
                this.Visibility = Visibility.Hidden;
                //Start checking every 2 secs at the process names - could be faster but this is a constant background process
                StartTimerLook(2000);
            }
        }

        private void StartTimerLook(Int32 interval) //Timer to look for new instances setup and start
        {
            timer2 = new DispatcherTimer();  timer2.Tick += timerLook_Tick;
            timer2.Interval = new TimeSpan(0, 0, 0, 0, interval);   timer2.Start();
        }

        private void timerLook_Tick(object sender, EventArgs e)
        {   //Every timer interval check to see if a process is present with the Ttile name flag 
            Process[] myNameFlagProcesses = Process.GetProcessesByName(my_Visible_exe_Name);

            for (int i = 0; i < myNameFlagProcesses.Length; i++)
            {
                if (myNameFlagProcesses[i].MainWindowTitle == my_exe_Name_Flag) //If name flag is seen ...
                {   //... then wake up
                    TextBox1.Text += "\r\n Saw the other window";
                    this.Visibility = Visibility.Visible;
                    this.Title = my_Visible_exe_Name; //Set the Title text to flag a visible state
                    this.Show();
                    this.Activate();
                    timer2.Stop();
                }
            }

        }

        private void QuitButton_Click(object sender, RoutedEventArgs e)
        {
            reallyCloseThisProgram = true;   Close();
        }
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM