Как сделать так , чтобы эта программа работала в фоновом режиме?
Я написал такой код:
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 шт):
Если я правильно понял, вы хотите написать в блокнот не выводя его на передний план. Если это так, то для решения данной задачи вам надо полностью отказаться от фокусировки окна. Сейчас ваш код работает по принципу:
- Вывожу на передний экран (
SetForegroundWindow
) нужное окно. - Нажимаю на нужные кнопки.
- Надеюсь, что окно было в фокусе и его не перекрыло что-то еще.
Логика того, что у вас примерно должно быть:
- Нахожу нужное окно.
- В этом окне нахожу нужный элемент (в нашем случае текстовое поле блокнота).
- Посылаю данному элементу нужные команды.
И тут, как я думаю, вы уже заметили, что подход индивидуальный, ибо у каждой программы свои внутренние компоненты, даже блокноты отличаются. Поэтому, я вам покажу лишь пример того, как можно поступить, а дальше уже подстраивайте под свои требования.
И так, начнем...
Подготовим проект. Я не особый любитель писать все WinAPI методы руками, из-за чего буду использовать генератор кода. Вы можете все перечисленные методы написать ручками, в интернете полно их реализаций и применения, ну а если хотите как и я, то:
- Устанавливайте пакет
Microsoft.Windows.CsWin32
- В проект добавляйте файл
NativeMethods.txt
(пока пустой) - В свойствах проекта включайте "небезопасный код" (свойства - сборка - галка на "небезопасный код").
- Устанавливайте пакет
Проект подготовили, супер, приступаем к реализации. Для начала нам надо найти окно блокнота, сделать это можно несколькими способами, самые простые и понятные, это:
- Найти окно по его заголовку (WinAPI метод -
FindWindow
) - Найти процесс по названию
.exe
(как вы это делаете) и от туда взятьHandle
окна. Этим способом я и воспользуюсь, взяв свойствоproc.MainWindowHandle
.
- Найти окно по его заголовку (WinAPI метод -
Окно нашли, теперь нам надо найти само текстовое поле. Если мы откроем
Spy++
(или аналогWinSpy++
) и начнем захватывать блокнот, то увидим что-то такое:Как видите, часть окна, которая отвечает за ввод текста, называется
RichEditD2DPT
(в старых версиях она вроде называлась простоEdit
), вот именно ее нам и надо получить, но как? Способа тут 2:Использовать WinAPI
FindWindowEx
, для этого в файлNativeMethods.txt
пишемFindWindowEx
и пересобираем проект. Далее пишем что-то тако:var editHandle = PInvoke.FindWindowEx((HWND)proc.MainWindowHandle, default, "Edit", null);
Запускаем, смотрим значение. Если там сплошные
0
, то найти не удалось, ну а если сработало, то супер. В старых версиях блокнота, классEdit
лежит вроде как дочерний объект самого окна, в новых это походу не так, ибо не находит...Перебрать при помощи
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
.
Осталось дело за малым, отправить в найденное текстовое поле нужные команды. И тут опять у нас есть варианты:
SendMessage
- Отправка "сообщения" с нужными данными и дожидаемся выполнения.PostMessage
- Аналогично предыдущему, но только "асинхронно", функция не ждет успешной отправки.SendInput
- Отправляет "импут" (клавиатура/мышь) в активное окно. В данном случае он нам не подходит, привел лишь для того, чтоб вы понимали, что "под капотом"SendKeys.SendWait()
.- Есть еще ряд разновидностей, таких как
SendNotifyMessage
,SendMessageTimeout
, и др., тут уж сами развлекайтесь с этим зоопарком)
Для данного примера я возьму
SendMessage
, собственно, добавляем его в файлNativeMethods.txt
и вызываем с нужными параметрами.
Чуть подробней про SendMessage
Данная функция принимает 4 параметра:
Хендлер окна/класса куда отправляем, с этим все понятно.
Само сообщение. Вот тут уже вариантов уйма, советую их изучить. Интересные для нас, это
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 использовать не стоит (часто видел примеры выше перечисленных функций именно с ним).