Как сделать так , чтобы эта программа работала в фоновом режиме?

Я написал такой код:

using System.Diagnostics;
using System.Runtime.InteropServices;

namespace AutoPoster
{
    public partial class Form1 : Form
    {
       
        private bool isWriting { get; set; }
        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.F9)
            {
                MessageBox.Show("staring");
                
                WriteThis("Test");
            }
            else if (e.KeyCode == Keys.F4)
            {
                ShowWindow(this.Handle, 1);
                MessageBox.Show("stopping");
                isWriting = false;
            }
        }
        private void button1_Click(object sender, EventArgs e)
        {

        }
        public Form1()
        {
            InitializeComponent();

        }
        private void Form1_Load(object sender, EventArgs e)
        {

        }
        [DllImport("user32.dll")]
        static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
        [DllImport("user32.dll")]
        static extern IntPtr FindWindowEx(IntPtr parentHandle, int childAfter, string className, string windowTitle);
        [DllImport("user32.dll")]
        static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, string lParam);

        const int WM_CHAR = 0x0102;

        [DllImport("user32.dll")]
        public static extern int SetForegroundWindow(IntPtr hWnd);
        [STAThread]

        private void WriteThis(string letter)
        {
            isWriting = true;
            while (isWriting)
            {
                Process[] processes = Process.GetProcessesByName("notepad");
                foreach (Process proc in processes)
                {
                    SetForegroundWindow(proc.MainWindowHandle);
                    Thread.Sleep(1000);
                    SendKeys.SendWait("T");
                    SendKeys.SendWait($"{letter}");
                    SendKeys.SendWait("{ENTER}");
                    Thread.Sleep(10000);

                }
            }

        }

    }

}

В нем есть метод WriteThis который в определенном окне имитирует нажатия клавиш. При нажатии f9 он начинает работать а при нажатии f4 перестает. Мне нужно сделать так, чтобы во время выполнения данного метода я не видел как он открывает окно и выполняет действия. То есть программа должна работать в фоновом режиме. Как можно переделать код или добавить что-то чтобы осуществить это? Про isWriting знаю, что не очень хорошо сделал.


Ответы (1 шт):

Автор решения: EvgeniyZ

Если я правильно понял, вы хотите написать в блокнот не выводя его на передний план. Если это так, то для решения данной задачи вам надо полностью отказаться от фокусировки окна. Сейчас ваш код работает по принципу:

  • Вывожу на передний экран (SetForegroundWindow) нужное окно.
  • Нажимаю на нужные кнопки.
  • Надеюсь, что окно было в фокусе и его не перекрыло что-то еще.

Логика того, что у вас примерно должно быть:

  • Нахожу нужное окно.
  • В этом окне нахожу нужный элемент (в нашем случае текстовое поле блокнота).
  • Посылаю данному элементу нужные команды.

И тут, как я думаю, вы уже заметили, что подход индивидуальный, ибо у каждой программы свои внутренние компоненты, даже блокноты отличаются. Поэтому, я вам покажу лишь пример того, как можно поступить, а дальше уже подстраивайте под свои требования.

И так, начнем...

  1. Подготовим проект. Я не особый любитель писать все WinAPI методы руками, из-за чего буду использовать генератор кода. Вы можете все перечисленные методы написать ручками, в интернете полно их реализаций и применения, ну а если хотите как и я, то:

    • Устанавливайте пакет Microsoft.Windows.CsWin32
    • В проект добавляйте файл NativeMethods.txt (пока пустой)
    • В свойствах проекта включайте "небезопасный код" (свойства - сборка - галка на "небезопасный код").
  2. Проект подготовили, супер, приступаем к реализации. Для начала нам надо найти окно блокнота, сделать это можно несколькими способами, самые простые и понятные, это:

    • Найти окно по его заголовку (WinAPI метод - FindWindow)
    • Найти процесс по названию .exe (как вы это делаете) и от туда взять Handle окна. Этим способом я и воспользуюсь, взяв свойство proc.MainWindowHandle.
  3. Окно нашли, теперь нам надо найти само текстовое поле. Если мы откроем Spy++ (или аналог WinSpy++) и начнем захватывать блокнот, то увидим что-то такое:

    Spy++

    Как видите, часть окна, которая отвечает за ввод текста, называется RichEditD2DPT (в старых версиях она вроде называлась просто Edit), вот именно ее нам и надо получить, но как? Способа тут 2:

    1. Использовать WinAPI FindWindowEx, для этого в файл NativeMethods.txt пишем FindWindowEx и пересобираем проект. Далее пишем что-то тако:

      var editHandle = PInvoke.FindWindowEx((HWND)proc.MainWindowHandle, default, "Edit", null);

      Запускаем, смотрим значение. Если там сплошные 0, то найти не удалось, ну а если сработало, то супер. В старых версиях блокнота, класс Edit лежит вроде как дочерний объект самого окна, в новых это походу не так, ибо не находит...

    2. Перебрать при помощи EnumChildWindows все классы окна и найти нужное по имени. Вот этим путем я и пойду. Для этого в файл NativeMethods.txt добавлю новой строкой EnumChildWindows и GetClassName, далее напишу следующий код:

      private HWND FindHandle(HWND hWnd, params string[] names)
      {
          HWND result = default;
          PInvoke.EnumChildWindows(hWnd, (handle, lParam) =>
          {
              unsafe
              {
                  fixed (char* classNameChars = new char[256])
                  {
                      var length = PInvoke.GetClassName(handle, classNameChars, 256);
                      string className = new string(classNameChars);
                      if (names.Contains(className))
                      {
                          result = handle;
                          return false;
                      }
                  }
              }
      
              return true;
          }, 0);
      
          return result;
      }
      

      Метод относительно прост, если знать как работает WinAPI) Вызываем PInvoke.EnumChildWindows, передав в него хандлер окна, метод от нас требует делегат с двумя значениями хандлер класса и его параметры, этот делегат он будет вызывать. Внутри, при вызове, вызываем PInvoke.GetClassName, в который передаем идентификатор класса, а также куда будет записано его имя и размер этого буфера (я взял 256). Далее просто преобразовываем в строку, и если это имя соответствует тому, что в массиве искомых, то сохраняем полученный handle и завершаем EnumChildWindows вернув делегатом false. В конце возвращаем полученный handle.

  4. Осталось дело за малым, отправить в найденное текстовое поле нужные команды. И тут опять у нас есть варианты:

    • SendMessage - Отправка "сообщения" с нужными данными и дожидаемся выполнения.
    • PostMessage - Аналогично предыдущему, но только "асинхронно", функция не ждет успешной отправки.
    • SendInput - Отправляет "импут" (клавиатура/мышь) в активное окно. В данном случае он нам не подходит, привел лишь для того, чтоб вы понимали, что "под капотом" SendKeys.SendWait().
    • Есть еще ряд разновидностей, таких как SendNotifyMessage, SendMessageTimeout, и др., тут уж сами развлекайтесь с этим зоопарком)

    Для данного примера я возьму SendMessage, собственно, добавляем его в файл NativeMethods.txt и вызываем с нужными параметрами.

Чуть подробней про SendMessage

Данная функция принимает 4 параметра:

  1. Хендлер окна/класса куда отправляем, с этим все понятно.

  2. Само сообщение. Вот тут уже вариантов уйма, советую их изучить. Интересные для нас, это

    • WM_GETTEXT - Получает текст.
    • WM_SETTEXT - Меняет полностью текст на заданный.
    • WM_CHAR - Отправляет указанный символ.
    • WM_KEYDOWN - "Зажимает" указанную клавишу.
    • WM_KEYUP - "Отжимает" указанную клавишу.

3 и 4. Это аргументы, которые соответствуют конкретному сообщению.

Собственно, теперь несколько примеров:

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

  • Добавляем в NativeMethods.txt новую строку WM_CHAR.

Сам код:

var text = "Hello, world!";
foreach (char chr in text)
{
    PInvoke.SendMessage(editHandle, PInvoke.WM_CHAR, chr, 0);
}

Замена текста в окне

  • Добавляем в NativeMethods.txt строку со значением WM_SETTEXT

Код:

unsafe
{
    fixed (char* textPtr = "Новый текст".ToCharArray())
    {
        var res = PInvoke.SendMessage(editHandle, PInvoke.WM_SETTEXT, 0, (IntPtr)textPtr);
    }
}

"Нажимаем" клавишу

  • Добавляем в NativeMethods.txt строки WM_KEYDOWN, WM_KEYUP, VIRTUAL_KEY

Для примера, переход на новую строку:

PInvoke.SendMessage(editHandle, PInvoke.WM_KEYDOWN, (uint)VIRTUAL_KEY.VK_RETURN, 0);
PInvoke.SendMessage(editHandle, PInvoke.WM_KEYUP, (uint)VIRTUAL_KEY.VK_RETURN, 0);

Обращу внимание, это не зажатие клавиши, это всего лишь отправка сообщения о том, что клавиша зажата, из-за чего каждая программа будет вести себя по разному, но на всякий, не забывайте отправлять событие "отжатия". Если нужно прям эмулировать зажатия клавиши, то возвращайтесь к фокусировке окна и используйте SendInput.

Получение текста окна

  • В NativeMethods.txt добавляем WM_GETTEXTLENGTH, WM_GETTEXT

Код:

var bufferSize = (int)PInvoke.SendMessage(editHandle, PInvoke.WM_GETTEXTLENGTH, 0, 0);
char[] buffer = new char[bufferSize + 1];
unsafe
{
    fixed (char* bufferPtr = buffer)
    {
        PInvoke.SendMessage(editHandle, PInvoke.WM_GETTEXT, (nuint)buffer.Length, (IntPtr)bufferPtr);
        var text = new string(buffer);
    }
}

Вот собственно и краткий экскурс в мир WinAPI.


Ах да, от unsafe можно попробовать отказаться, код замены текста будет выглядеть примерно так тогда:

var textPtr = Marshal.StringToHGlobalAuto("Текст");
var res = PInvoke.SendMessage(editHandle, PInvoke.WM_SETTEXT, 0, textPtr);
Marshal.FreeHGlobal(textPtr);

остальное по аналогии. Ну а если будете методы самостоятельно писать, то не забывайте про безопасность, оптимизацию, и так далее. К примеру StringBuilder использовать не стоит (часто видел примеры выше перечисленных функций именно с ним).

→ Ссылка