Как передать метод в качестве обратного вызова для вызова Windows API? - PullRequest
8 голосов
/ 07 мая 2010

Я хотел бы передать метод класса в качестве обратного вызова функции WinAPI.Возможно ли это, и если да, то как?

Пример случая установки таймера:

TMyClass = class
public
  procedure TimerProc(Wnd:HWND; uMsg:DWORD; idEvent:PDWORD; dwTime:DWORD);
  procedure DoIt;
end;
[...]
procedure TMyClass.DoIt;
begin
  SetTimer(0, 0, 8, @TimerProc);  // <-???- that's what I want to do (last param)
end;

Спасибо за помощь!

Редактировать :Цель состоит в том, чтобы указать метод этого класса в качестве обратного вызова.Никакой процедуры вне класса.

Edit2 : Я ценю всю вашу помощь, но пока у метода нет «TMyClass».перед его именем это не то, что я ищу.Раньше я делал это таким образом, но задавался вопросом, смогу ли я полностью остаться в объектно-ориентированном мире.Указатель магии приветствуется.

Ответы [ 6 ]

9 голосов
/ 07 мая 2010

Madshi имеет процедуру MethodToProcedure .Он находится в "madTools.pas", который находится в пакете "madBasic".Если вы используете его, вы должны изменить соглашение о вызовах для «TimerProc» на stdcall, и процедура DoIt станет такой:

TMyClass = class
private
  Timer: UINT;
  SetTimerProc: Pointer;
[...]

procedure TMyClass.DoIt;
begin
  SetTimerProc := MethodToProcedure(Self, @TMyClass.TimerProc);
  Timer := SetTimer(0, 0, 8, SetTimerProc);
end;
// After "KillTimer(0, Timer)" is called call:
// VirtualFree(SetTimerProc, 0, MEM_RELEASE);


Я никогда не пробовал, но я думаю, что можно также попытаться дублироватькод в "classses.MakeObjectInstance" для передачи процедур других типов, кроме TWndMethod.

5 голосов
/ 07 мая 2010

Какую версию Delphi вы используете?

В последних вы можете использовать статические методы класса для этого:

TMyClass = class
public
  class procedure TimerProc(Wnd:HWND; uMsg:DWORD; idEvent:PDWORD; dwTime:DWORD); stdcall; static;
  procedure DoIt;
end;
[...]
procedure TMyClass.DoIt;
begin
  SetTimer(0, 0, 8, @TimerProc);  
end;
2 голосов
/ 07 мая 2010

Процедура TimerProc должна быть стандартной процедурой, а не указателем метода.

Указатель метода - это действительно пара указатели; первый хранит адрес метода, а второй хранит ссылка на объект метод принадлежит

Редактировать

Это может быть столько ООП, сколько вы получите. Все неприятные вещи скрыты от всех, кто использует ваш TMyClass.

unit Unit2;

interface

type
  TMyClass = class
  private
    FTimerID: Integer;
    FPrivateValue: Boolean;
  public
    constructor Create;
    destructor Destroy; override;
    procedure DoIt;
  end;

implementation

uses
  Windows, Classes;

var
  ClassList: TList;

constructor TMyClass.Create;
begin
  inherited Create;
  ClassList.Add(Self);
end;

destructor TMyClass.Destroy;
var
  I: Integer;
begin
  I := ClassList.IndexOf(Self);
  if I <> -1 then
    ClassList.Delete(I);
  inherited;
end;

procedure TimerProc(Wnd:HWND; uMsg:DWORD; idEvent:PDWORD; dwTime:DWORD); stdcall;
var
  I: Integer;
  myClass: TMyClass;
begin
  for I := 0 to Pred(ClassList.Count) do
  begin
    myClass := TMyClass(ClassList[I]);
    if myClass.FTimerID = Integer(idEvent) then
      myClass.FPrivateValue := True;
  end;
end;

procedure TMyClass.DoIt;
begin
  FTimerID := SetTimer(0, 0, 8, @TimerProc);  // <-???- that's what I want to do (last param)
end;

initialization
  ClassList := TList.Create;

finalization
  ClassList.Free;

end.

Редактировать: (как указано в glob)

Не забудьте добавить соглашение о вызовах stdcall.

1 голос
/ 07 мая 2010

Я использовал MakeObjectInstance несколько раз, чтобы сделать то же самое. Вот статья на эту тему: Как использовать функцию-член класса VCL для обратного вызова Win32

1 голос
/ 07 мая 2010

Ответ на ваше второе редактирование:

Если вы хотите получить ответ, содержащий указатель на экземпляр TMyClass, вам может не повезти. По сути, процедура, вызываемая Windows, имеет определенную подпись и не является объектным методом. Вы не можете напрямую обойти это, даже с помощью магии __closure или procedure of object, за исключением случаев, описанных ниже и в других ответах. Почему?

  • Windows не знает, что это объектный метод, и хочет вызвать процедуру с определенной сигнатурой.

  • Указатель больше не является простым указателем - он имеет две половины: экземпляр объекта и метод. Необходимо сохранить Self, а также метод.

Кстати, я не понимаю, что не так с коротким падением за пределы объектно-ориентированного мира. Код не-OO не обязательно грязный , если используется хорошо.

Оригинальный ответ перед редактированием:

Это невозможно в точности так, как вы пытаетесь это сделать. Метод, который нужен SetTimer, должен точно соответствовать подписи TIMERPROC - см. Документацию MSDN . Это простая необъектная процедура.

Однако метод TMyClass.DoIt является объектным методом. На самом деле он состоит из двух частей: объекта, для которого он вызывается, и самого метода. В Delphi это "procedure of object" или "closure" ( читайте о процедурных типах здесь ). Таким образом, подписи несовместимы, и вы не можете сохранить экземпляр объекта, который вам нужен для вызова метода объекта. (Существуют также проблемы соглашения о вызовах - стандартные методы Delphi реализуются с использованием соглашения fastcall, тогда как TIMERPROC указывает CALLBACK, который из памяти является макросом, который расширяется до stdcall. Подробнее о вызове условности и особенно fastcall .)

Итак, что ты делаешь? Вам необходимо отобразить ваш не объектно-ориентированный обратный вызов в объектно-ориентированный код .

Есть несколько способов, и самый простой из них:

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

type TMyObjectProc = procedure of object;
var pfMyProc : TMyObjectProc;

Затем инициализируйте pfMyProc до nil. В TMyClass.DoIt установите для pfMyProc значение @DoIt, то есть теперь оно указывает на процедуру DoIt в контексте этого конкретного экземпляра TMyClass. Ваш обратный вызов может затем вызвать этот метод.

(Если вам интересно, переменные класса, имеющие такой процедурный тип, как это, хранят обработчики событий внутри. Свойства OnFoo объекта VCL являются указателями на объектные процедуры.)

К сожалению, эта процедурная архитектура не объектно-ориентирована, но именно так она и должна быть.

Вот как может выглядеть какой-то полный код (я не работаю с компилятором, поэтому он может работать не так, как написано, но он должен быть закрыт):

type TMyObjectProc = procedure of object;
var pfMyProc : TMyObjectProc;

initialization
  pfMyProc = nil;

procedure MyTimerCallback(hWnd : HWND; uMsg : DWORD; idEvent : PDWORD; dwTime : DWORD); stdcall;
begin
  if Assigned(pfMyProc) then begin
    pfMyProc(); // Calls DoIt, for the object that set the timer
    pfMyProc = nil;
  end;
end;

procedure TMyClass.MyOOCallback;
begin
  // Handle your callback here
end;

procedure TMyClass.DoIt;
begin
  pfMyProc = @MyOOCallback;
  SetTimer(0, 0, 8, @ MyTimerCallback);
end;

Другим способом было бы воспользоваться тем, что у вашего таймера есть уникальный идентификатор. Сохраните отображение между идентификатором таймера и объектом. В обратном вызове преобразуйте идентификатор в указатель и вызовите метод объекта.

Редактировать: Я заметил комментарий к другому ответу, предлагающий использовать адрес вашего объекта в качестве идентификатора таймера. Это будет работать, но это потенциально опасный хак, если у вас два объекта с одним и тем же адресом в разное время и вы не вызываете KillTimer. Я использовал этот метод, но лично мне он не нравится - я думаю, что лучше вести дополнительную бухгалтерию хранения карты (ID таймера, указателя объекта). Это действительно сводится к личному стилю.

0 голосов
/ 07 мая 2010
TMyClass = class
public
  procedure DoIt;
  procedure DoOnTimerViaMethod;
end;

var MyReceiverObject: TMyClass;

[...]

procedure TimerProc(Wnd:HWND; uMsg:DWORD; idEvent:PDWORD; dwTime:DWORD); stdcall:
begin
  if Assigned(MyReceiverObject) then 
    MyReceiverObject.DoOnTimerViaMethod;
end;

procedure TMyClass.DoIt;
begin
  MyReceiverObject := Self;
  SetTimer(0, 0, 8, @TimerProc);  // <-???- that's what I want to do (last param)
end;

Не идеально.Следите за потоками, перезаписью переменных и т. Д. Но это делает свою работу.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...