Привет. Долго сомневался в нужности такой статьи, хоть и подробного материала в сети довольно мало, учитывая сложность выше минимума - я не уверен что статья будет пользоваться вниманием. Впрочем посмотрим. В процессе, в сети нашёл видео уроки создания трейнера на visual_Studio C++ где кодер использует объектно ориентированный код, моё мнение - классы нужны в более-менее сложном проекте , каковым трейнер всё таки не является и добавлять ему сложности в виде объектов не вижу смысла. Вполне хватает линейного кода и всяких структур данных. Но тут дело вкуса. Мы же создадим класс только для работы с потоками.
Я далеко не спец и программирую для удовольствия, так что строго не судите, нет - я писал разные программки на разных "языЦах" - но вот трейнер впервые. Трейнер будет для SCUM сам не знаю почему ;-), - просто в это время я его скачал и изучал карту, ну и хотелось преимуществ в виде патронов, бессмертия и прочего. Впрочем игра может быть любая. На момент начала написания статьи, версия SCUM была v0.85, теперь - 20.10.2023 SCUM.v0.9.113.75065
Будьте вежливы и оставляйте пжлст. ссылку на материал при копипасте и т.д. Предложения о корректировках и критика приветствуются, постараюсь учесть.
Наверное статья будет разбита на несколько частей, иначе получится слишком много в одной куче. На скриншотах, адреса ассемблерных команд могут не соответствовать следующим скриншотам, так как статья пишется не за один раз и приходится перезагружать игру и отладчик, но старался такого не допускать.
Трейнер будет патчить исполняемый код целевого процесса (игры) в памяти. Мы пишем приложение X64 и откуда у вас Delphi я понятия не имею ;-) - может Delphi Community Edition ?
-------------------
Ссылки на части: _часть 2 _часть 3
_получаем патч-байты для SCUM.v0.9.113.75065
-------------------
Примерный план:
1) В первой части набросаем форму с каким нибудь скином для красоты. Для этого используем компонент - alphaControls. Кому не нужно - может и без него, а остальные могут посмотреть статью по "растриаливанию" этого компонента, так как он триальный. Получим необходимые для работы привилегии. Напишем функцию поиска PID нужного процесса, а так же функции ожидания появления нужного нам процесса с помощью таймера в случае его (процесса) отсутствия. В целях интереса - будет реализована работа и без таймера, с кнопкой активировать. Так же, для мониторинга целевого процесса, напишем ожидание его завершения с помощью функции WaitForSingleObject запущенной в новом потоке. (А можно и без неё, используя всё тот же таймер для мониторинга наличия процесса). И обработаем активацию/деактивацию слайдеров.
2) Во второй части напишем функции доступа к коду процесса в памяти, получения данных о модулях подключённых к процессу, получим данные PE-заголовка главного модуля и вычислим размеры секции кода, так как патчить будем именно в ней. Для хранения разных данных будем использовать в основном записи (record). Так как трейнер производит изменение кода в памяти по каким то адресам - логично предположить что их надо от чего то отсчитывать. Мне понравилась идея поиска сигнатур кода по маске, это и будет реализовано. То есть, мы находим адрес уникальной сигнатуры (последовательность байт) и отсчитываем смещение для патча от неё, но можно и от начала секции.
3) В третьей части - будем менять права на секцию кода, будет реализована функция пропатчивания, запускаемая с помощью слайдеров на форме. Создадим структуру (запись - record) содержащую все данные для патчей, а так же структуру для сохранения оригинальных байт перед патчем. Выяснять что и где нужно патчить будем с помощью отладчика x64Dbg , Cheat Engine и других утилит по ходу. ( Art Money тоже подойдёт ;-)
В процессе в числе прочего, будут созданы функции конвертеры: массив байт в строку, из hex-строки в массив байт и возможно прочие (в первой версии трейнера - наколбасил приличное кол-во конвертеров). Используемые в основном для тестового вывода значений, адресов и прочего.
Так как материала получается довольно много - возможно частей будет больше. Приводимый код лучше изучать в notepad++.
4) В дополнительной части получим данные для патчей из SCUM.
Часть 1
Запускаем Delphi, создаём новый Windows VCL Application. Если нужно оформление - кидаем на форму AlphaTools->TsSkinManager - в котором прописываем в SkinDirectory путь к папке со скинами, а в SkinName выбираем тему оформления. А чтобы тема стала встроенной - правой мышкой по sSkinManager->Internal skins... и в окне скинов добавить нужную тему. Кидаем на форму таймер, ну и прочие нужные контролы.
У меня так:
Чекбокс "без таймера" и кнопка "активировать" - были добавлены ради эксперимента, для работы без таймера.
Надписи на форме - TsLabelFX , слайдеры - TsSlider, статусбар -TsStatusBar. Изображение PNG - добавлено с помощью TsImage. А Memo - для вывода тестовой информации - потом его можно убрать.
Слайдеры я настроил так: галочка Reversed - True - чтоб положение вкл было в право, SkinData->CustomColor - True - чтоб установить свои цвета для положений вкл/выкл. Цвета например такие: Color -$00483A25 и ColorOn $007BD098 для вкл. Подгоняем цвета, ширину и высоту окна, ровняем надписи, компилируем и проверяем как выглядит. Если всё устраивает кодим дальше.
Начнём постепенно писать функции и пополнять их в процессе полезным кодом.
Для начала нам нужно получить Debug привилегии для возможности чтения/записи в памяти процессов, далее получить список работающих процессов и выбрать из них нужный. Отлаживать будем на процессе Notepad.exe , а после уже переключимся на игру.
Объявим константу gameName сразу после подключения модулей uses:
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, sSkinManager, Vcl.ExtCtrls, acImage, acPNG, Vcl.StdCtrls, sLabel, Vcl.ComCtrls, sStatusBar, sPanel, acSlider, tlhelp32, sButton, sCheckBox;
const
gameName = 'notepad.exe';
и создадим приватные глобальные переменные для pid и имени процесса игры в блоке private юнита:
private
//хранят: pid и имя - процесса игры
progID:DWORD;
progName:string;
Сразу инициализируем их в событии FormCreate главной формы. Выбираем в инспекторе объектов (Object Inspector) нашу форму (Form1) и на вкладке Events создаём событие OnCreate (FormCreate), где инициализируем наши переменные:
procedure TForm1.FormCreate(Sender: TObject);
begin
progName:=''; progID:=0;
end;
Я обычно отделяю свои функции от создаваемых средой - блоками комментариев, и пишу их внутри. Напишем функцию получения привилегий, сразу за строками implementation {$R *.dfm} :
implementation
{$R *.dfm}
//////////////// функции ////////////////
function SetPrivilege(prochwnd: HWND; privilegeName: string; enable: boolean): boolean;
var
tpPrev, tp : TTokenPrivileges;
token : THandle;
dwRetLen : DWord;
begin
result := False;
try
if prochwnd = 0 then OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, token)
else OpenProcessToken(prochwnd, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, token);
tp.PrivilegeCount := 1;
if LookupPrivilegeValue(nil, pchar(privilegeName), tp.Privileges[0].LUID) then
begin
if enable then
tp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED
else
tp.Privileges[0].Attributes := 0;
dwRetLen := 0;
result := AdjustTokenPrivileges(token, False, tp, SizeOf(tpPrev), tpPrev, dwRetLen);
end;
finally
try
CloseHandle(token);
except
end;
end;
end;
////////// конец функций ////////////////
и выполним функцию SetPrivilege при запуске программы в FormCreate:
procedure TForm1.FormCreate(Sender: TObject);
begin
progName:=''; progID:=0;
if SetPrivilege(0, 'SeDebugPrivilege', true) then sStatusBar1.SimpleText:='привелегии получены'
else sStatusBar1.SimpleText:='привелегии не получены';
end;
В строке статуса выводим результат получения привилегий, можно скомпилировать и проверить в работе.
Когда я писал трейнер, то с начала реализовал поиск и мониторинг процесса по таймеру, а потом без - с кнопкой активации, здесь мы начнём работу с кнопкой активировать, а потом сделаем и работу по таймеру.
Добавим переменную флаг активации трейнера к private глобальным переменным:
sButton1ActivateTrainer:Boolean; //флаг статуса для ручной активации трейнера без таймера
а в FormCreate добавим её инициализацию: sButton1ActivateTrainer:=false;
sCheckBox1 будет делать кнопку не активной/активной при выборе работы с таймером или без. Напишем код для чекбокса. В FormCreate добавляем:
if not sCheckBox1.Checked then begin
//Timer1.Enabled := True; //таймер пока закомментируем
sButton1.Enabled:=false;
sCheckBox1.Caption:='таймер включён';
end else begin
sButton1.Enabled:=true;
sCheckBox1.Caption:='без таймера';
end;
Далее создаём событие чекбокса OnClick для активации/деактивации кнопки, в инспекторе объектов выбираем sCheckBox1 и на вкладке Events двойной щелчок в OnClick и в sCheckBox1Click пишем код:
procedure TForm1.sCheckBox1Click(Sender: TObject);
begin
if sCheckBox1.Checked then begin
sButton1.Enabled:=true;
//Timer1.Enabled := false; //таймер пока закомментируем
sCheckBox1.Caption:='без таймера';
end else begin
sButton1.Enabled:=false;
//Timer1.Enabled := true; //таймер пока закомментируем
sCheckBox1.Caption:='таймер включён';
end;
end;
Строки для таймера на будущее, пока их закомментируем. Компилируем проект и проверяем работу чекбокса.
Теперь реализуем код кнопки активации трейнера - Button1Click, где будут происходить подготовительные действия: поиск процесса игры, заполнение рабочих структур нужными данными и прочее. В инспекторе объектов создаём процедуру TForm1.sButton1Click нашей кнопки. И так как у нас есть флаг статуса для ручной активации трейнера - sButton1ActivateTrainer , создаём такой код:
procedure TForm1.sButton1Click(Sender: TObject);
begin
if not sButton1ActivateTrainer then begin //активировать трейнер
sButton1.Caption:='деактивировать';
sButton1ActivateTrainer:=true;
end else begin //деактивировать трейнер
sButton1.Caption:='активировать';
sButton1ActivateTrainer:=false;
end;
end;
который будем пополнять кодом. Скомпилировали, проверили, колбасим дальше. Давайте ка докинем на форму Memo для проверки всяких переменных, данных и установим её ScrollBars в ssBoth для возможности прокрутки большого количества строк.
Теперь напишем функцию поиска процесса игры, которая в положительном случае заполнит переменные progID и progName :
Добавляем в верху, в блок подключения модулей uses через запятую tlhelp32 , объявляем функцию в блоке type там где TForm1 = class(TForm) , function findProcessGame(prcssName: string):boolean; , добавим константу с именем искомого процесса gameName = 'notepad.exe' , добавим флаг processFindedFlag - отображающий статус нахождения процесса игры и глобальную переменную globali в блок public для визуализации работы таймера :
unit Unit1;
interface
uses
Winapi.Windows, ... , tlhelp32;
const
gameName = 'notepad.exe'; // 'SCUM.exe';
type
TForm1 = class(TForm)
sSkinManager1: TsSkinManager;
...
procedure sButton1Click(Sender: TObject);
function findProcessGame(prcssName: string):boolean;
private
//хранят: pid и имя - процесса игры
progID:DWORD;
progName:string;
//флаг статуса для ручной активации трейнера без таймера
sButton1ActivateTrainer:Boolean;
processFindedFlag:Boolean; //если флаг - true - процесс игры найден
public
globali: Integer; //счётчик для отладки
end;
Добавим инициализацию processFindedFlag в FormCreate :
procedure TForm1.FormCreate(Sender: TObject);
begin
progName:=''; progID:=0;
sButton1ActivateTrainer:=false;
processFindedFlag:=False;
...
и пишем функцию поиска процесса ( внутри блока отделённого комментарием // функции // )
function TForm1.findProcessGame(prcssName: string):boolean;
var
hSnapShot: THandle; ProcInfo: TProcessEntry32; findFlag:Boolean;
prcssName2:string;
begin
findFlag:=False; hSnapShot:=0;
hSnapShot := CreateToolHelp32Snapshot(TH32CS_SNAPPROCESS, 0);
prcssName2:=LowerCase(prcssName);
if (hSnapShot <> THandle(-1)) then
begin
ProcInfo.dwSize := SizeOf(ProcInfo);
if (Process32First(hSnapshot, ProcInfo)) then
begin
if LowerCase(ProcInfo.szExeFile) = prcssName2 then
begin
//если нашли - заполним глобальные переменные искомого процесса
progName:=ProcInfo.szExeFile; progID:=ProcInfo.th32ProcessID;
if progID > 0 then begin
findFlag:=true;
CloseHandle(hSnapShot); hSnapShot:=0;
//Result:=true;
end;
end else begin //если не найден на первом проходе - повторяем поиск
while (Process32Next(hSnapShot, ProcInfo)) do
if LowerCase(ProcInfo.szExeFile) = prcssName2 then
begin
//если нашли - заполним глобальные переменные искомого процесса
progName:=ProcInfo.szExeFile; progID:=ProcInfo.th32ProcessID;
Memo1.Lines.Add(LowerCase(ProcInfo.szExeFile) + ' || ' + prcssName2); //для тестового вывода
if progID > 0 then begin
findFlag:=true;
CloseHandle(hSnapShot); hSnapShot:=0;
//Result:=true;
end;
end;
end;
end;
end;
if hSnapShot > 0 then CloseHandle(hSnapShot);
if findFlag then Result:=true else Result:=False;
end;
Реализуем вызов findProcessGame по клику на кнопке sButton1Click:
procedure TForm1.sButton1Click(Sender: TObject);
begin
if not sButton1ActivateTrainer then begin //активировать трейнер
if findProcessGame(gameName) then begin //заполняется progID
SCheckBox1.Enabled:=false;
sLabelFX1.Caption:='pid: ' + inttostr(progID);
sLabelFX2.Caption:='имя: '+ progName;
processFindedFlag:=true; //установка флага наличия процесса
sStatusBar1.SimpleText:='готов к работе';
//активируем слайдеры
activDeactivSliders('X', clGrayText, True);
sButton1.Caption:='деактивировать';
sButton1ActivateTrainer:=true;
end else sStatusBar1.SimpleText:='процесс '+ gameName + ' не найден.';
end else begin //деактивировать трейнер
SCheckBox1.Enabled:=true;
activDeactivSliders('', $483A25, False);
sStatusBar1.SimpleText:='процесс ' + gameName + ' не найден ';
if processFindedFlag then //был найден до этого (сброс всех полей - заполненных ранее)
begin
progName:=''; progID:=0;
sLabelFX1.Caption:='pid:';
sLabelFX2.Caption:='имя:';
processFindedFlag:=false; //сброс флага наличия процесса
end;
sButton1.Caption:='активировать';
sButton1ActivateTrainer:=false;
end;
end;
Компилируем и проверяем предварительно запустив notepad.exe. Запускаем Диспетчер задач и сравниваем PID процесса (ИД процесса) Блокнот (notepad.exe) в диспетчере и трейнере. Если столбец ИД процесса в диспетчере отсутствует, правой мышкой в области названий столбцов и поставить нужную галочку.
Теперь реализуем работу с таймером, он будет запускать функцию поиска процесса при его отсутствии или пропаже. Как вы заметили что таймер у нас будет работать при деактивированном чекбоксе ( без таймера ). Для визуализации работы таймер я добавил глобальную переменную globali в раздел public нашего юнита, чтоб можно было видеть количество тактов таймера в любой функции. Установим таймеру свойства Enable в False, а interval тика таймера в 1000 ( 1 секунда ). У него всего одно событие OnTimer, создаём его:
procedure TForm1.Timer1Timer(Sender: TObject);
begin
inc(globali); //+1 к переменной глобального счётчика ( декоративный )
if findProcessGame(gameName) then begin //процесс игры найден , так же заполняет progID
if not processFindedFlag then
begin
if progID > 0 then begin
sLabelFX1.Caption:='pid: ' + inttostr(progID);
sLabelFX2.Caption:='имя: '+ progName;
//для кнопки
sButton1.Caption:='деактивировать';
sButton1ActivateTrainer:=true;
timer1.Interval:=5000; //увеличим интервал до 5 секунд
processFindedFlag:=true; //процесс игры найден
sStatusBar1.SimpleText:='готов к работе';
end;
end;
end else begin //процесс игры не найдена
sStatusBar1.SimpleText:='процесс ' + gameName + ' не найден ';
//для кнопки
sButton1.Caption:='активировать';
sButton1ActivateTrainer:=false;
if processFindedFlag then //был найден до этого
begin
progName:=''; progID:=0;
sLabelFX1.Caption:='pid:'; sLabelFX2.Caption:='имя:';
timer1.Interval:=2000; //меняем интервал до 2 секунд
//тестовый вывод в Memo
Form1.Memo1.Lines.Add(' ***** искомый процесс не найден ***** ' );
processFindedFlag:=false; //сброс флага наличия процесса
end;
//тестовый вывод в Memo
Memo1.Lines.Add(inttostr(globali) + ' ожидаем процесса игры..');
end;
end;
и раскомментируем в процедурах TForm1.FormCreate и TForm1.sCheckBox1Click строки, активации и деактивации таймера Timer1.Enabled := false; и Timer1.Enabled := true; Компилируем, смотрим в лог сообщений дельфы нет ли ошибок и запускаем. Так как таймер работает сразу после запуска ( при снятом чекбоксе ), нужно запустить notepad и найденный процесс отобразится в Memo. Если установить галку в чекбоксе - то таймер перестанет работать и сообщений в Memo не будет. При закрытии блокнота - таймер продолжит поиск нужного процесса. Ну и добавим событие FormClose где на всякий случай деактивируем таймер: ( возможно это и лишнее ;-)
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
timer1.Enabled:=False;
end;
Мне стало интересно реализовать мониторинг процесса с помощью функции WaitForSingleObject запущенной в отдельном потоке - ожидающей сигнала ( в нашем случае сигнал закрытия ) от наблюдаемого процесса. Таймер выключаем при появлении процесса, мониторинг происходит с помощью WaitForSingleObject , при исчезновении процесса снова включаем таймер. Кодим:
Для начала создадим флаг статуса потока ожидания flagExecuteThread в разделе private переменных нашего юнита:
flagExecuteThread: boolean; //флаг статуса потока ожидания
и инициализируем его в FormCreate flagExecuteThread:=false; , а использовать его мы будем в TForm1.Timer1Timer.
Теперь опишем класс потока для WaitForSingleObject, напишем его перед блоком var с объявлением формы var Form1: TForm1; , а сразу после Form1: TForm1; объявим переменную для этого класса - waitThread :
//класс отдельного потока для WaitForSingleObject
TWaitThread = class(TThread)
private
protected
procedure Execute; override;
end;
var
Form1: TForm1;
waitThread: TWaitThread;
не обращайте внимания на красное подчёркивание в procedure Execute; - мы его просто ещё не написали, о чём дэльфа и сигнализирует. Давайте напишем процедуру Execute для нашего класса потока TWaitThread:
///////////////////////////// TWaitThread ///////////////////////////
procedure TWaitThread.Execute;
var
retWait:Cardinal; prHandle: THandle;
begin
prHandle:=OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_ALL_ACCESS or PROCESS_VM_OPERATION, False, Form1.progID);
if prHandle > 0 then begin
retWait := WaitForSingleObject (prHandle, 1000);
Form1.Memo1.Lines.Add('поток запущен');
//создаём цикл для возможности прерывания WaitForSingleObject
while retWait > 0 do begin
retWait := WaitForSingleObject (prHandle, 2000);
Form1.Memo1.Lines.Add('_мониторим наличие процесса_');
if waitThread.Terminated then begin
break;
end;
end;
CloseHandle(prHandle);
if (retWait = 0) or (waitThread.Terminated) then begin
Form1.flagExecuteThread:=false;
if not Form1.sCheckBox1.Checked then begin
Form1.Timer1.Enabled:=true; //запустим таймер
Form1.Memo1.Lines.Add(' таймер активирован.');
end;
Form1.Memo1.Lines.Add('поток ожидания остановлен');
Form1.Memo1.Lines.Add(' приложение ' + gameName +' закрыто.');
end;
end;
end;
///////////////// TWaitThread END////////////////////////////
я ограничил её комментарием для большей наглядности в коде - это на выбор. При исчезновении наблюдаемого процесса - WaitForSingleObject вернёт ноль, даже при падении или некорректном его прерывании и наш наблюдающий поток прекратит выполнение.
Напишем запуск нашего потока из procedure TForm1.Timer1Timer , добавляем код обработки потока в блок if progID > 0 then begin ... и его else ветку:
procedure TForm1.Timer1Timer(Sender: TObject);
begin
inc(globali); //инкремент глобального счётчика
if findProcessGame(gameName) then begin //процесс игры найден , так же заполняет progID
if not processFindedFlag then
begin
if progID > 0 then begin
sLabelFX1.Caption:='pid: ' + inttostr(progID);
sLabelFX2.Caption:='имя: '+ progName;
//для кнопки
sButton1.Caption:='деактивировать';
sButton1ActivateTrainer:=true;
timer1.Interval:=5000; //увеличим интервал до 5 секунд
processFindedFlag:=true; //процесс игры найден
sStatusBar1.SimpleText:='готов к работе';
//запускаем ожидание закрытия наблюдаемого процесса
if not flagExecuteThread then begin
waitThread := TWaitThread.Create(False);
waitThread.FreeOnTerminate:=true;
waitThread.Priority := tpLower;
flagExecuteThread:=true;
Memo1.Lines.Add('Запуск потока ожидания завершения ' + gameName);
//выключаем таймер за ненадобностью, так как процесс отслеживает WaitForSingleObject в функции Execute Класса TWaitThread
//обратно - включение происходит в функции Execute Класса TWaitThread после уничтожения потока
Timer1.Enabled:=false;
Memo1.Lines.Add('Таймер деактивирован');
end;
end;
end;
end else begin //процесс игры не найдена
sStatusBar1.SimpleText:='процесс ' + gameName + ' не найден ';
//для кнопки
sButton1.Caption:='активировать';
sButton1ActivateTrainer:=false;
if processFindedFlag then //был найден до этого
begin
progName:=''; progID:=0;
sLabelFX1.Caption:='pid:'; sLabelFX2.Caption:='имя:';
timer1.Interval:=2000; //меняем интервал до 2 секунд
Form1.Memo1.Lines.Add(' ***** искомый процесс не найден ***** ' );
processFindedFlag:=false; //сброс флага наличия процесса
end;
//сброс флага работы потока ожидания
if flagExecuteThread then begin
flagExecuteThread:=false;
Memo1.Lines.Add('Остановка потока ожидания завершения ' + gameName );
end;
//тестовый вывод в Memo
Memo1.Lines.Add(inttostr(globali) + ' ожидаем процесса игры..');
end;
end;
Проверяем, исправляем ошибки, компилируем и испытываем. Наш трейнер теперь должен определять и появление и исчезновение целевого процесса.
Теперь научим слайдеры активироваться/деактивироваться в зависимости от наличия нужного процесса и создадим одну функцию обрабатывающую клик на всех слайдерах кроме первого (sSlider1) , а первый слайдер будет включать/выключать все разом. Начнём с первого, создадим для sSlider1 событие OnSliderSChange и заполним кодом:
procedure TForm1.sSlider1SliderChange(Sender: TObject);
var
sldrClickName:TsSlider; flagOn:boolean;
begin
sldrClickName :=TsSlider(Sender); flagOn:=false;
if sldrClickName.Name = 'sSlider1' then begin
//sLabelFX3 - подпись к sSlider1 (у вас может быть другая)
if sldrClickName.SliderOn then begin
sLabelFX3.Caption:='выключить все';
flagOn:=true;
end else begin
sLabelFX3.Caption:='включить все';
flagOn:=false;
end;
if not flagOn = sSlider2.SliderOn then sSlider2.Click;
if not flagOn = sSlider3.SliderOn then sSlider3.Click;
if not flagOn = sSlider4.SliderOn then sSlider4.Click;
if not flagOn = sSlider5.SliderOn then sSlider5.Click;
if not flagOn = sSlider6.SliderOn then sSlider6.Click;
end;
end;
Теперь напишем процедуру активации/деактивации слайдеров, объявим её в блоке type ( там где TForm1 = class(TForm) ) нашего юнита :
procedure activDeactivSliders(sldrCaption:string; sldrColor:TColor; enabl:boolean);
и пишем реализацию:
procedure TForm1.activDeactivSliders(sldrCaption:string; sldrColor:TColor; enabl:boolean);
var i:Integer; tmpSldr:TsSlider;
begin
for i:=0 to Form1.ControlCount-1 do
begin //ищем слайдеры на форме
if Form1.Controls[i] is TsSlider then begin
tmpSldr:=nil;
tmpSldr:=(Form1.Controls[i] as TsSlider);
tmpSldr.Color:= sldrColor; //clGrayText;
tmpSldr.SliderCaptionOff:=sldrCaption;
tmpSldr.ShowCaption := enabl;
if not enabl then tmpSldr.SliderOn:= False; //выключаем если disable
Form1.Controls[i].Enabled:=enabl; // активация/деактивация
tmpSldr:=nil;
end;
end;
end;
а результат этой процедуры у нас зависит от наличия процесса игры, то есть в Timer1Timer, в блоке if progID > 0 then begin после строки sStatusBar1.SimpleText:='готов к работе'; пишем:
//активируем слайдеры
activDeactivSliders('X', clGrayText, True);
а в блоке else ( относящемуся к этому же if progID > 0 then ) - end else begin //не найдена ещё один вызов с деактивацией слайдеров (можно в начале блока):
//деактивируем слайдеры
activDeactivSliders('', $483A25, False); //$483A25 - цвет
Можно скомпилировать и проверить работу. При появлении процесса блокнота - слайдеры становятся активны и меняют цвет на посветлее, состояние ВКЛ отображается зелёным. Когда процесс блокнота пропадает - слайдеры выключаются и деактивируются.
Теперь сделаем то же для кнопки активировать в процедуре TForm1.sButton1Click в блок if findProcessGame(gameName) then begin после sStatusBar1.SimpleText:='готов к работе'; добавляем код:
//активируем слайдеры
activDeactivSliders('X', clGrayText, True);
а в его else часть:
activDeactivSliders('', $483A25, False);
Собираем, проверяем, исправляем ;-)
Для первой части достаточно - вторая следует.
Полный код Unit1.pas для первой части.
Критика и поправки приветствуются. Всем удачи.