Страницы

Поиск по вопросам

понедельник, 6 января 2020 г.

c#. Winforms. Single-Instance и активация окна при повторном запуске

#c_sharp #net #winforms


Разрабоатываю WinForms  приложение как Single-Instance. При первом вызове запускается
как бы «Ядро» приложения, которое затем создает и отображает определенное окно. При
последующем запуске приложение проверяет, запущено ли уже приложение и в положительном
случае посылает сообщение ядру, передает ему опредленные данные и закрывается. Ядро
получает сообщение с параметром, обрабатывает его пределенным образом и создает новую
форму и ее оторбражает.
Реализаций подобного есть несколько. Я ипользовал мютексы для проверки существования
запущенного приложения и данные передаю при помощи именованных pipes (также пробовал
с Remoting). Но есть одна проблема, которая доставляет большое неудобство, а именно,
вторая инстанция приложения не получает фокус ввода. Чего я уже только не пробовал,
приложение при актвации лишь мигает в таскбаре, а фокус ввода находится там, откуда
оно было вызвано.

Я уже и с виндовc API игрался, никак не выходит. Например вызов SetForegroundWindow(HandleRef
hWnd) возвращает False. И ничего нельзя сделать.

Как можно решить задачу?



Добавление:

Если кто-то захочет помочь, как базис можно использовать например вот эту статью
и код. Хотя я использую несколько другой подход, в этом приложении тоже проявляется
описанная мною проблема: 

http://blogs.microsoft.co.il/maxim/2010/02/13/single-instance-application-manager/

Просто запускайте приложение из файлового проводника при помощи "Enter", и увидете
что фокус ввода при повторном запуске остается в проводнике. Экспериментировать, как
я понимаю, нужно здесь:

private static void SingleInstanceCallback(object sender, InstanceCallbackEventArgs args)
{
    if (args == null || _mainFrm == null) return;
    Action d = (bool x) =>
    {
        _mainFrm.ApendArgs(args.CommandLineArgs);
        _mainFrm.Activate(x);
    };
    _mainFrm.Invoke(d, true);
}

    


Ответы

Ответ 1



Дело в том, что у функции SetForegroundWindow в Windows есть некоторые ограничения. Изменить активное окно может только процесс, который уже владеет активным окном, у которого недавно было свернуто активное окно, в который недавно осуществлялся ввод с помощью клавиатуры или мыши, либо которому явно выдано разрешение на изменение активного окна с помощью AllowSetForegroundWindow (есть и другие случаи, см. документацию). Поэтому данная схема работает не всегда. Обходится это довольно просто (на Win7, по крайней мере), перед активацией окна сначала его свернуть и восстановить: private void NamedPipeManager_ReceiveString(string obj) { main.WindowState = FormWindowState.Minimized; main.WindowState = FormWindowState.Normal; main.Activate(); } Но в целом, вся схема с мьютексами и каналами для проверки единственности окна программы кажется слишком сложной и ненужной. То же самое можно реализовать гораздо проще, не упираясь в эти ограничения. Существующее окно программы можно найти через Process.MainWindowHandle, а для передачи командной строки использовать WM_COPYDATA: using System; using System.IO; using System.Linq; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Windows.Forms; using System.Diagnostics; using System.Runtime.InteropServices; namespace WindowsFormsTest1 { public partial class Form1 : Form { [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); const int ShowWindow_Restore = 9; [DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, ref COPYDATASTRUCT lParam); [StructLayout(LayoutKind.Sequential)] public struct COPYDATASTRUCT { public IntPtr dwData; public int cbData; public IntPtr lpData; } const uint WM_COPYDATA = 0x004A; public Form1() { InitializeComponent(); Process this_process = Process.GetCurrentProcess(); //найти все процессы с таким же именем Process[] other_processes = Process.GetProcessesByName(this_process.ProcessName).Where(pr => pr.Id != this_process.Id).ToArray(); foreach (var pr in other_processes) { pr.WaitForInputIdle(1000); //на случай, если процесс еще не загрузился //берем первый процесс с окном IntPtr hWnd = pr.MainWindowHandle; if (hWnd == IntPtr.Zero) continue; //отправляем командную строку string command_line = "/activate"; var cds = new COPYDATASTRUCT(); cds.dwData = (IntPtr)1; cds.cbData = (command_line.Length + 1) * 2; cds.lpData = Marshal.StringToHGlobalUni(command_line); SendMessage(hWnd, WM_COPYDATA, IntPtr.Zero, ref cds); Marshal.FreeHGlobal(cds.lpData); //активируем окно и выходим ShowWindow(hWnd, ShowWindow_Restore); SetForegroundWindow(hWnd); Environment.Exit(0); } //если ничего не найдено, продолжаем работу } protected override void WndProc(ref Message m) { if (m.Msg == WM_COPYDATA) { COPYDATASTRUCT data = new COPYDATASTRUCT(); data = (COPYDATASTRUCT)Marshal.PtrToStructure(m.LParam, data.GetType()); textBox1.Text = Marshal.PtrToStringUni(data.lpData); } base.WndProc(ref m); } private void Form1_Load(object sender, EventArgs e) { textBox1.Focus(); textBox1.Select(); } } }

Ответ 2



Странно. Activate() у меня отрабатывает. Сейчас приведу свой пример программы. Вот PipeManager, который я использовал. Писал не я, сам нашёл где-то (уже не помню где). public class NamedPipeManager { public static string NamedPipeName { get; } = "MyApplicationName"; public event Action ReceiveString; private const string EXIT_STRING = "__EXIT__"; private BackgroundWorker backgroundWorker; public void Start() { backgroundWorker = new BackgroundWorker(); backgroundWorker.WorkerReportsProgress = true; backgroundWorker.DoWork += BackgroundWorker_DoWork; backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged; backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted; backgroundWorker.RunWorkerAsync(); } public void Stop() { Write(EXIT_STRING); } private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { backgroundWorker.Dispose(); } private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { ReceiveString?.Invoke(e.UserState as string); } private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { while (true) { string result; using (var server = new NamedPipeServerStream(NamedPipeName)) { server.WaitForConnection(); using (StreamReader reader = new StreamReader(server)) result = reader.ReadToEnd(); } if (result == EXIT_STRING) break; backgroundWorker.ReportProgress(0, result); } } public static bool Write(string[] text, int connectTimeout = 300) { using (var client = new NamedPipeClientStream(NamedPipeName)) { try { client.Connect(connectTimeout); } catch { return false; } if (!client.IsConnected) return false; using (StreamWriter writer = new StreamWriter(client)) { foreach (var a in text) writer.Write(a + '\n'); writer.Flush(); } } return true; } public static bool Write(string text, int connectTimeout = 300) { using (var client = new NamedPipeClientStream(NamedPipeName)) { try { client.Connect(connectTimeout); } catch { return false; } if (!client.IsConnected) return false; using (StreamWriter writer = new StreamWriter(client)) { writer.Write(text); writer.Flush(); } } return true; } } В Program.cs я добавляю 3 статичных поля: static Mutex Mutex; static NamedPipeManager NamedPipeManager; static Form1 main; В Main() я в начало добавляю это: Mutex = new Mutex(true, "MyApplicationName", out bool Is); if (!Is) { NamedPipeManager.Write("1"); Application.Exit(); return; } NamedPipeManager = new NamedPipeManager(); NamedPipeManager.ReceiveString += NamedPipeManager_ReceiveString; NamedPipeManager.Start(); Естественно в поле main я храню свою основную форму. А событием ReceiveString вызываю этот метод: private static void NamedPipeManager_ReceiveString(string obj) { if (main.WindowState == FormWindowState.Minimized) main.WindowState = FormWindowState.Normal; main.Activate(); } Специально сейчас попробовал по быстрому написать этот пример. Запускаю один раз приложение - появляется окно. Потом фокусируюсь обратно на проводник, запускаю - опять открывается первое окно. Если сверну и запускаю с проводника, то опять же первоначальное окно запускается.

Ответ 3



Натолкнулся на одно решение и оно у меня на Windows 10 с небольшими изменениями работает: https://stackoverflow.com/questions/6319568/how-to-bring-a-form-already-shown-up-to-the-very-foreground-and-focus-it/22737820#22737820 [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr ProcessId); [DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach); public static void xActivateAndBringToFront(this Form form) { // activate window var currentForegroundWindow = GetForegroundWindow(); var thisWindowThreadId = GetWindowThreadProcessId(form.Handle, IntPtr.Zero); var currentForegroundWindowThreadId = GetWindowThreadProcessId(currentForegroundWindow, IntPtr.Zero); AttachThreadInput(currentForegroundWindowThreadId, thisWindowThreadId, true); form.Activate(); // or: SetForegroundWindow(form.Handle); AttachThreadInput(currentForegroundWindowThreadId, thisWindowThreadId, false); // set window to front form.TopMost = true; form.TopMost = false; } Дополнение после подсказок от MSDN.WhiteKnight. MSDN.WhiteKnight указал на то, что применение метода AttachThreadInput может вызывать в некоторых случаях проблемы. Краткий поиск в интернете подтверждает это. Поэтому, с его же подсказки, я попробовал применить метод AllowSetForegroundWindow. С успехом! А именно, приложение при первом запуске сохраняет ID своего процесса в реестре: Application.UserAppDataRegistry.SetValue(CORE_PROCESS_ID, Process.GetCurrentProcess().Id); Затем, при повторном запуске приложение перед посылкой сообщения ядру вызывает метод AllowSetForegroundWindow: int processID = (int)Application.UserAppDataRegistry.GetValue(CORE_PROCESS_ID); bool b = AllowSetForegroundWindow(processID); // .. сообщение ядру .. // .. выход .. Ядро создает и отображает окно, окно получает фокус ввода как положено. Не нужно даже как-то принудительно подымать окно. По сути form.Activate() достаточно, но даже этот вызов не нужен, если окно создается и отображается в регламентированном порядке через Show(), ShowDialog() или Application.Run(form).

Комментариев нет:

Отправить комментарий